393 files
This commit is contained in:
@@ -15,6 +15,7 @@ from .const import (
|
||||
CONF_TASK_ENDS_UNTIL,
|
||||
CONF_TASK_INTERVAL_UNIT,
|
||||
CONF_TASK_SEASON_MONTHS,
|
||||
SCHEDULE_OFFSET_MAX_DAYS,
|
||||
)
|
||||
from .helpers.dates import INTERVAL_UNITS
|
||||
from .helpers.entity_analyzer import EntityAnalyzer
|
||||
@@ -29,6 +30,27 @@ from .helpers.threshold_calculator import ThresholdCalculator, ThresholdSuggesti
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Flow error key for a last-performed date after today (strings.json config +
|
||||
# options ``error``).
|
||||
ERROR_LAST_PERFORMED_FUTURE = "last_performed_future"
|
||||
|
||||
|
||||
def is_future_date(value: Any) -> bool:
|
||||
"""True when ``value`` is an ISO date AFTER today (HA's time zone).
|
||||
|
||||
A last-performed date is a day the work WAS done: one in the future (the
|
||||
date picker reaches year 9999) overflowed the schedule math inside every
|
||||
refresh and kept the object in setup-retry (bug audit 2026-09-27). A
|
||||
malformed value is not "future" — the selectors already refuse it.
|
||||
"""
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .helpers.dates import parse_iso_date
|
||||
|
||||
parsed = parse_iso_date(str(value)) if value else None
|
||||
return parsed is not None and parsed > dt_util.now().date()
|
||||
|
||||
|
||||
# Calendar recurrence kinds offered in the config + options flows (Phase 4;
|
||||
# the calendar-entity kind since #187). Hardcoded English labels for the
|
||||
# weekday/occurrence sub-options keep the config-flow i18n surface small; the
|
||||
@@ -103,7 +125,9 @@ def calendar_schema(kind: str, current: dict[str, Any] | None = None) -> vol.Sch
|
||||
# (#83) ±N-day shift of the computed occurrence, on every calendar kind
|
||||
# ("two days before the last working day" = last_day + business + offset -2).
|
||||
fields[vol.Optional("offset", default=cur.get("offset", 0))] = selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(min=-15, max=15, step=1, mode=selector.NumberSelectorMode.BOX)
|
||||
selector.NumberSelectorConfig(
|
||||
min=-SCHEDULE_OFFSET_MAX_DAYS, max=SCHEDULE_OFFSET_MAX_DAYS, step=1, mode=selector.NumberSelectorMode.BOX
|
||||
)
|
||||
)
|
||||
return vol.Schema(fields)
|
||||
|
||||
@@ -159,6 +183,23 @@ def calendar_current(task: dict[str, Any]) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def select_default(value: Any, options: list[Any], fallback: Any = "") -> Any:
|
||||
"""A stored value made safe as a ``SelectSelector`` default.
|
||||
|
||||
HA validates the submitted form against the select's options, and a
|
||||
stored value the options no longer carry — a deleted user, a user
|
||||
promoted to admin (no longer on the non-admin list), the ``None`` the
|
||||
panel dialog persisted for "no rotation" — came back as the field's
|
||||
default and failed that validation on every save: the whole form was
|
||||
unsaveable (bug audit 2026-09-26). A multi-select keeps only its still
|
||||
valid members; a single select falls back to ``fallback``.
|
||||
"""
|
||||
allowed = {opt["value"] if isinstance(opt, dict) else opt for opt in options}
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [v for v in value if v in allowed]
|
||||
return value if value in allowed else fallback
|
||||
|
||||
|
||||
def interval_unit_selector() -> selector.SelectSelector:
|
||||
"""Shared days/weeks/months/years dropdown for the interval unit.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user