Add clock alarm scheduling with formatted alarm time options
This commit is contained in:
@@ -4,7 +4,14 @@ from __future__ import annotations
|
||||
|
||||
LABEL = "clock"
|
||||
TOOLTIP = "It tells the time. What did you think it did?"
|
||||
EDITABLE_PROPERTIES: tuple[str, ...] = ("title", "timeZone", "use24Hour", "topOfHourAnnounce")
|
||||
EDITABLE_PROPERTIES: tuple[str, ...] = (
|
||||
"title",
|
||||
"timeZone",
|
||||
"use24Hour",
|
||||
"topOfHourAnnounce",
|
||||
"alarmEnabled",
|
||||
"alarmTime",
|
||||
)
|
||||
CAPABILITIES: tuple[str, ...] = ("editable", "carryable", "deletable", "usable")
|
||||
USE_SOUND: str | None = None
|
||||
EMIT_SOUND = "sounds/clock.ogg"
|
||||
@@ -55,12 +62,24 @@ TIME_ZONE_OPTIONS: tuple[str, ...] = (
|
||||
"Pacific/Pago_Pago",
|
||||
"UTC",
|
||||
)
|
||||
DEFAULT_PARAMS: dict = {"timeZone": DEFAULT_TIME_ZONE, "use24Hour": False, "topOfHourAnnounce": True}
|
||||
PARAM_KEYS: tuple[str, ...] = ("timeZone", "use24Hour", "topOfHourAnnounce")
|
||||
DEFAULT_PARAMS: dict = {
|
||||
"timeZone": DEFAULT_TIME_ZONE,
|
||||
"use24Hour": False,
|
||||
"topOfHourAnnounce": True,
|
||||
"alarmEnabled": False,
|
||||
"alarmTime": "",
|
||||
}
|
||||
PARAM_KEYS: tuple[str, ...] = ("timeZone", "use24Hour", "topOfHourAnnounce", "alarmEnabled", "alarmTime")
|
||||
|
||||
PROPERTY_METADATA: dict[str, dict[str, object]] = {
|
||||
"title": {"valueType": "text", "tooltip": "Display name spoken and shown for this item.", "maxLength": 80},
|
||||
"timeZone": {"valueType": "list", "tooltip": "Timezone used when the clock speaks time.", "options": list(TIME_ZONE_OPTIONS)},
|
||||
"use24Hour": {"valueType": "boolean", "tooltip": "Use 24 hour format instead of AM/PM."},
|
||||
"topOfHourAnnounce": {"valueType": "boolean", "tooltip": "Automatically announce time at the top of each hour."},
|
||||
"alarmEnabled": {"valueType": "boolean", "tooltip": "Enable one daily alarm announcement at the configured alarm time."},
|
||||
"alarmTime": {
|
||||
"valueType": "text",
|
||||
"tooltip": "Alarm time. Uses 24-hour HH:MM when 24 hour format is on, otherwise H:MM AM/PM.",
|
||||
"maxLength": 8,
|
||||
},
|
||||
}
|
||||
|
||||
52
server/app/items/types/clock/time_format.py
Normal file
52
server/app/items/types/clock/time_format.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""Clock alarm time parsing/format helpers shared by validation and runtime checks."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
_TWELVE_HOUR_RE = re.compile(r"^(0?[1-9]|1[0-2]):([0-5]\d)\s*([AaPp][Mm])$")
|
||||
_TWENTY_FOUR_HOUR_RE = re.compile(r"^([01]?\d|2[0-3]):([0-5]\d)$")
|
||||
|
||||
|
||||
def parse_alarm_time_for_mode(value: object, use_24_hour: bool) -> tuple[int, int] | None:
|
||||
"""Parse alarm time using one explicit mode and return `(hour24, minute)`."""
|
||||
|
||||
raw = str(value or "").strip()
|
||||
if not raw:
|
||||
return None
|
||||
if use_24_hour:
|
||||
match = _TWENTY_FOUR_HOUR_RE.fullmatch(raw)
|
||||
if not match:
|
||||
return None
|
||||
return int(match.group(1)), int(match.group(2))
|
||||
match = _TWELVE_HOUR_RE.fullmatch(raw)
|
||||
if not match:
|
||||
return None
|
||||
hour = int(match.group(1))
|
||||
minute = int(match.group(2))
|
||||
meridiem = match.group(3).upper()
|
||||
if meridiem == "AM":
|
||||
hour24 = 0 if hour == 12 else hour
|
||||
else:
|
||||
hour24 = 12 if hour == 12 else hour + 12
|
||||
return hour24, minute
|
||||
|
||||
|
||||
def parse_alarm_time_flexible(value: object) -> tuple[int, int] | None:
|
||||
"""Parse alarm time as either 12-hour or 24-hour format and return `(hour24, minute)`."""
|
||||
|
||||
parsed = parse_alarm_time_for_mode(value, use_24_hour=True)
|
||||
if parsed is not None:
|
||||
return parsed
|
||||
return parse_alarm_time_for_mode(value, use_24_hour=False)
|
||||
|
||||
|
||||
def format_alarm_time_for_mode(hour24: int, minute: int, use_24_hour: bool) -> str:
|
||||
"""Format one parsed alarm time tuple as canonical 12-hour or 24-hour text."""
|
||||
|
||||
if use_24_hour:
|
||||
return f"{hour24:02d}:{minute:02d}"
|
||||
meridiem = "AM" if hour24 < 12 else "PM"
|
||||
hour12 = hour24 % 12 or 12
|
||||
return f"{hour12}:{minute:02d} {meridiem}"
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from ....models import WorldItem
|
||||
from ...helpers import keep_only_known_params, parse_bool_like_or_none
|
||||
from .definition import DEFAULT_TIME_ZONE, PARAM_KEYS, TIME_ZONE_OPTIONS
|
||||
from .time_format import format_alarm_time_for_mode, parse_alarm_time_flexible
|
||||
|
||||
|
||||
def validate_update(_item: WorldItem, next_params: dict) -> dict:
|
||||
@@ -19,7 +20,20 @@ def validate_update(_item: WorldItem, next_params: dict) -> dict:
|
||||
top_of_hour_announce = parse_bool_like_or_none(next_params.get("topOfHourAnnounce"))
|
||||
if top_of_hour_announce is None:
|
||||
raise ValueError("topOfHourAnnounce must be on/off.")
|
||||
alarm_enabled = parse_bool_like_or_none(next_params.get("alarmEnabled"))
|
||||
if alarm_enabled is None:
|
||||
raise ValueError("alarmEnabled must be on/off.")
|
||||
alarm_time_raw = str(next_params.get("alarmTime", "") or "").strip()
|
||||
parsed_alarm = parse_alarm_time_flexible(alarm_time_raw) if alarm_time_raw else None
|
||||
if alarm_enabled and parsed_alarm is None:
|
||||
raise ValueError("alarmTime must be a valid time (HH:MM or H:MM AM/PM) when alarm is on.")
|
||||
if alarm_time_raw and parsed_alarm is None:
|
||||
raise ValueError("alarmTime must be a valid time (HH:MM or H:MM AM/PM).")
|
||||
next_params["timeZone"] = time_zone
|
||||
next_params["use24Hour"] = use_24_hour
|
||||
next_params["topOfHourAnnounce"] = top_of_hour_announce
|
||||
next_params["alarmEnabled"] = alarm_enabled
|
||||
next_params["alarmTime"] = (
|
||||
format_alarm_time_for_mode(parsed_alarm[0], parsed_alarm[1], use_24_hour) if parsed_alarm is not None else ""
|
||||
)
|
||||
return keep_only_known_params(next_params, PARAM_KEYS)
|
||||
|
||||
Reference in New Issue
Block a user