mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-15 22:10:20 +00:00
* Add time-based conditional visibility for cards * Move clearTimeout outside of scheduleUpdate * Add time string validation * Add time string validation * Remove runtime validation as config shouldnt allow bad values * Fix for midnight crossing * Cap timeout to 32-bit signed integer * Add listener tests * Additional tests * Format
132 lines
3.7 KiB
TypeScript
132 lines
3.7 KiB
TypeScript
import { TZDate } from "@date-fns/tz";
|
|
import { isBefore, isAfter, isWithinInterval } from "date-fns";
|
|
import type { HomeAssistant } from "../../types";
|
|
import { TimeZone } from "../../data/translation";
|
|
import { WEEKDAY_MAP } from "./weekday";
|
|
import type { TimeCondition } from "../../panels/lovelace/common/validate-condition";
|
|
|
|
/**
|
|
* Validate a time string format and value ranges without creating Date objects
|
|
* @param timeString Time string to validate (HH:MM or HH:MM:SS)
|
|
* @returns true if valid, false otherwise
|
|
*/
|
|
export function isValidTimeString(timeString: string): boolean {
|
|
// Reject empty strings
|
|
if (!timeString || timeString.trim() === "") {
|
|
return false;
|
|
}
|
|
|
|
const parts = timeString.split(":");
|
|
|
|
if (parts.length < 2 || parts.length > 3) {
|
|
return false;
|
|
}
|
|
|
|
// Ensure each part contains only digits (and optional leading zeros)
|
|
// This prevents "8:00 AM" from passing validation
|
|
if (!parts.every((part) => /^\d+$/.test(part))) {
|
|
return false;
|
|
}
|
|
|
|
const hours = parseInt(parts[0], 10);
|
|
const minutes = parseInt(parts[1], 10);
|
|
const seconds = parts.length === 3 ? parseInt(parts[2], 10) : 0;
|
|
|
|
if (isNaN(hours) || isNaN(minutes) || isNaN(seconds)) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
hours >= 0 &&
|
|
hours <= 23 &&
|
|
minutes >= 0 &&
|
|
minutes <= 59 &&
|
|
seconds >= 0 &&
|
|
seconds <= 59
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Parse a time string (HH:MM or HH:MM:SS) and set it on today's date in the given timezone
|
|
*
|
|
* Note: This function assumes the time string has already been validated by
|
|
* isValidTimeString() at configuration time. It does not re-validate at runtime
|
|
* for consistency with other condition types (screen, user, location, etc.)
|
|
*
|
|
* @param timeString The time string to parse (must be pre-validated)
|
|
* @param timezone The timezone to use
|
|
* @returns The Date object
|
|
*/
|
|
export const parseTimeString = (timeString: string, timezone: string): Date => {
|
|
const parts = timeString.split(":");
|
|
const hours = parseInt(parts[0], 10);
|
|
const minutes = parseInt(parts[1], 10);
|
|
const seconds = parts.length === 3 ? parseInt(parts[2], 10) : 0;
|
|
|
|
const now = new TZDate(new Date(), timezone);
|
|
const dateWithTime = new TZDate(
|
|
now.getFullYear(),
|
|
now.getMonth(),
|
|
now.getDate(),
|
|
hours,
|
|
minutes,
|
|
seconds,
|
|
0,
|
|
timezone
|
|
);
|
|
|
|
return new Date(dateWithTime.getTime());
|
|
};
|
|
|
|
/**
|
|
* Check if the current time matches the time condition (after/before/weekday)
|
|
* @param hass Home Assistant object
|
|
* @param timeCondition Time condition to check
|
|
* @returns true if current time matches the condition
|
|
*/
|
|
export const checkTimeInRange = (
|
|
hass: HomeAssistant,
|
|
{ after, before, weekdays }: Omit<TimeCondition, "condition">
|
|
): boolean => {
|
|
const timezone =
|
|
hass.locale.time_zone === TimeZone.server
|
|
? hass.config.time_zone
|
|
: Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
|
|
const now = new TZDate(new Date(), timezone);
|
|
|
|
// Check weekday condition
|
|
if (weekdays && weekdays.length > 0) {
|
|
const currentWeekday = WEEKDAY_MAP[now.getDay()];
|
|
if (!weekdays.includes(currentWeekday)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check time conditions
|
|
if (!after && !before) {
|
|
return true;
|
|
}
|
|
|
|
const afterDate = after ? parseTimeString(after, timezone) : undefined;
|
|
const beforeDate = before ? parseTimeString(before, timezone) : undefined;
|
|
|
|
if (afterDate && beforeDate) {
|
|
if (isBefore(beforeDate, afterDate)) {
|
|
// Crosses midnight (e.g., 22:00 to 06:00)
|
|
return !isBefore(now, afterDate) || !isAfter(now, beforeDate);
|
|
}
|
|
return isWithinInterval(now, { start: afterDate, end: beforeDate });
|
|
}
|
|
|
|
if (afterDate) {
|
|
return !isBefore(now, afterDate);
|
|
}
|
|
|
|
if (beforeDate) {
|
|
return !isAfter(now, beforeDate);
|
|
}
|
|
|
|
return true;
|
|
};
|