1717 NOTIF_TYPE_MANDATORY_PARENT_ALERT ,
1818 NOTIF_TYPE_MANDATORY_REMINDER ,
1919)
20+ from .coord_set_times import set_time_label
2021from .models import MandatoryMiss , parse_datetime
2122
2223# How often the escalation ladder is re-evaluated (FEAT-6).
@@ -100,6 +101,8 @@ async def async_recheck_mandatory_miss(self, chore_id: str, child_id: str, day:
100101 chore = self .get_chore (chore_id )
101102 if not chore or not getattr (chore , "mandatory" , False ):
102103 return 0
104+ if isinstance (getattr (chore , "times" , None ), list ) and chore .times :
105+ return await self .async_detect_set_time_misses (day = day )
103106 period_id = self ._effective_period_for (chore , child_id , day )
104107 if not self ._period_has_ended (period_id , day ):
105108 return 0
@@ -114,6 +117,9 @@ async def async_detect_mandatory_misses(self, period_id: str, day: date) -> int:
114117 continue
115118 if not getattr (chore , "enabled" , True ):
116119 continue
120+ # Set times have a miss per time (async_detect_set_time_misses).
121+ if isinstance (getattr (chore , "times" , None ), list ) and chore .times :
122+ continue
117123 if not self ._is_chore_scheduled_for_date (chore , day ):
118124 continue
119125 for child_id in self ._mandatory_owers (chore , day ):
@@ -183,7 +189,7 @@ async def async_apply_mandatory_penalty(self, miss_id: str) -> None:
183189 if miss is None :
184190 return
185191 chore = next ((c for c in self .storage .get_chores () if c .id == miss .chore_id ), None )
186- name = getattr (chore , "name " , "chore" )
192+ name = set_time_label (chore , getattr ( miss , "time_id " , "" )) if chore else "chore"
187193 if miss .penalty_points > 0 :
188194 await self .async_remove_points (
189195 miss .child_id ,
@@ -275,7 +281,10 @@ async def async_escalate_mandatory_misses(self, now: datetime | None = None) ->
275281 for miss in self .storage .get_mandatory_misses ():
276282 if miss .due_date != today_iso :
277283 continue # only escalate same-day misses (skips midnight backfill)
278- if self ._child_completed_today (miss .chore_id , miss .child_id , now .date ()):
284+ if getattr (miss , "time_id" , "" ):
285+ if self .set_time_miss_done (miss , now .date ()):
286+ continue
287+ elif self ._child_completed_today (miss .chore_id , miss .child_id , now .date ()):
279288 continue
280289 created = parse_datetime (miss .created_at )
281290 if created is None :
@@ -298,7 +307,7 @@ async def async_escalate_mandatory_misses(self, now: datetime | None = None) ->
298307 ctx = {
299308 "child_id" : miss .child_id ,
300309 "child_name" : getattr (child , "name" , "" ),
301- "chore_name" : getattr (chore , "name " , "chore" ) ,
310+ "chore_name" : set_time_label (chore , getattr ( miss , "time_id " , "" )) if chore else "chore" ,
302311 }
303312 for stage in range (miss .escalation_stage + 1 , target + 1 ):
304313 if stage in (1 , 2 ):
@@ -357,7 +366,13 @@ def arm_mandatory_schedules(self) -> None:
357366
358367 @callback
359368 def _escalation_tick (self , now : datetime ) -> None :
360- self .hass .async_create_task (self .async_escalate_mandatory_misses (now ))
369+ # Set times close all through the day, so their misses are checked
370+ # on the same tick rather than at fixed period ends.
371+ self .hass .async_create_task (self ._async_set_time_then_escalate (now ))
372+
373+ async def _async_set_time_then_escalate (self , now : datetime ) -> None :
374+ await self .async_detect_set_time_misses (now )
375+ await self .async_escalate_mandatory_misses (now )
361376
362377 def _make_mandatory_period_cb (self , period_id : str ):
363378 @callback
@@ -389,13 +404,16 @@ async def async_catchup_mandatory_misses(self) -> None:
389404 for hour , minute , period_id in self ._mandatory_period_end_times ():
390405 if dt_time (hour , minute ) <= cur :
391406 await self .async_detect_mandatory_misses (period_id , today )
407+ await self .async_detect_set_time_misses (now )
392408
393409 async def async_detect_anytime_mandatory_misses (self ) -> None :
394410 """Midnight: raise misses for incomplete mandatory 'anytime' chores for the
395411 day that just ended, then prune the postpone override map (new day starts clean)."""
396412 self ._ensure_mandatory_state ()
397413 yesterday = (dt_util .now () - timedelta (days = 1 )).date ()
398414 await self .async_detect_mandatory_misses ("anytime" , yesterday )
415+ # A set time that closed in the last minutes of yesterday.
416+ await self .async_detect_set_time_misses (day = yesterday )
399417 self .mandatory_postpone = {}
400418
401419 # ---- maintenance -------------------------------------------------------
@@ -421,7 +439,8 @@ def mandatory_misses_state(self) -> list[dict]:
421439 out = []
422440 for m in self .storage .get_mandatory_misses ():
423441 d = m .to_dict ()
424- d ["chore_name" ] = getattr (chores .get (m .chore_id ), "name" , "" )
442+ chore = chores .get (m .chore_id )
443+ d ["chore_name" ] = set_time_label (chore , getattr (m , "time_id" , "" )) if chore else ""
425444 d ["child_name" ] = getattr (children .get (m .child_id ), "name" , "" )
426445 out .append (d )
427446 return out
0 commit comments