Skip to content

Commit c99f95a

Browse files
committed
Move root functions to common area, add time condition handlers
1 parent 4dc67a3 commit c99f95a

File tree

10 files changed

+203
-72
lines changed

10 files changed

+203
-72
lines changed

src/common/condition/extract.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { Condition } from "../../panels/lovelace/common/validate-condition";
2+
3+
/**
4+
* Extract media queries from conditions recursively
5+
*/
6+
export function extractMediaQueries(conditions: Condition[]): string[] {
7+
return conditions.reduce<string[]>((array, c) => {
8+
if ("conditions" in c && c.conditions) {
9+
array.push(...extractMediaQueries(c.conditions));
10+
}
11+
if (c.condition === "screen" && c.media_query) {
12+
array.push(c.media_query);
13+
}
14+
return array;
15+
}, []);
16+
}
17+
18+
/**
19+
* Extract time conditions from conditions recursively
20+
*/
21+
export function extractTimeConditions(
22+
conditions: Condition[]
23+
): { after?: string; before?: string; weekdays?: string[] }[] {
24+
return conditions.reduce<
25+
{ after?: string; before?: string; weekdays?: string[] }[]
26+
>((array, c) => {
27+
if ("conditions" in c && c.conditions) {
28+
array.push(...extractTimeConditions(c.conditions));
29+
}
30+
if (c.condition === "time") {
31+
array.push({
32+
after: c.after,
33+
before: c.before,
34+
weekdays: c.weekdays,
35+
});
36+
}
37+
return array;
38+
}, []);
39+
}

src/common/condition/listeners.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { listenMediaQuery } from "../dom/media_query";
2+
import type { HomeAssistant } from "../../types";
3+
import type { Condition } from "../../panels/lovelace/common/validate-condition";
4+
import { checkConditionsMet } from "../../panels/lovelace/common/validate-condition";
5+
import { extractMediaQueries, extractTimeConditions } from "./extract";
6+
import { calculateNextTimeUpdate } from "./time-calculator";
7+
8+
/**
9+
* Helper to setup media query listeners for conditional visibility
10+
*/
11+
export function setupMediaQueryListeners(
12+
conditions: Condition[],
13+
hass: HomeAssistant,
14+
addListener: (unsub: () => void) => void,
15+
onUpdate: (conditionsMet: boolean) => void
16+
): void {
17+
const mediaQueries = extractMediaQueries(conditions);
18+
19+
if (mediaQueries.length === 0) return;
20+
21+
// Optimization for single media query
22+
const hasOnlyMediaQuery =
23+
conditions.length === 1 &&
24+
conditions[0].condition === "screen" &&
25+
!!conditions[0].media_query;
26+
27+
mediaQueries.forEach((mediaQuery) => {
28+
const unsub = listenMediaQuery(mediaQuery, (matches) => {
29+
if (hasOnlyMediaQuery) {
30+
onUpdate(matches);
31+
} else {
32+
const conditionsMet = checkConditionsMet(conditions, hass);
33+
onUpdate(conditionsMet);
34+
}
35+
});
36+
addListener(unsub);
37+
});
38+
}
39+
40+
/**
41+
* Helper to setup time-based listeners for conditional visibility
42+
*/
43+
export function setupTimeListeners(
44+
conditions: Condition[],
45+
hass: HomeAssistant,
46+
addListener: (unsub: () => void) => void,
47+
onUpdate: (conditionsMet: boolean) => void
48+
): void {
49+
const timeConditions = extractTimeConditions(conditions);
50+
51+
if (timeConditions.length === 0) return;
52+
53+
timeConditions.forEach((timeCondition) => {
54+
const scheduleUpdate = () => {
55+
const delay = calculateNextTimeUpdate(
56+
hass,
57+
timeCondition.after,
58+
timeCondition.before,
59+
timeCondition.weekdays
60+
);
61+
62+
if (delay === undefined) return;
63+
64+
const timeoutId = setTimeout(() => {
65+
const conditionsMet = checkConditionsMet(conditions, hass);
66+
onUpdate(conditionsMet);
67+
// Reschedule for next boundary
68+
scheduleUpdate();
69+
}, delay);
70+
71+
// Store cleanup function
72+
addListener(() => clearTimeout(timeoutId));
73+
};
74+
75+
scheduleUpdate();
76+
});
77+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { TZDate } from "@date-fns/tz";
2+
import {
3+
startOfDay,
4+
addDays,
5+
addMinutes,
6+
differenceInMilliseconds,
7+
} from "date-fns";
8+
import type { HomeAssistant } from "../../types";
9+
import { TimeZone } from "../../data/translation";
10+
import { parseTimeString } from "../datetime/check_time";
11+
12+
/**
13+
* Calculate milliseconds until next time boundary for a time condition
14+
* @param hass Home Assistant object
15+
* @param after Optional time string (HH:MM) for after condition
16+
* @param before Optional time string (HH:MM) for before condition
17+
* @param weekdays Optional array of weekdays to match
18+
* @returns Milliseconds until next boundary, or undefined if no boundaries
19+
*/
20+
export function calculateNextTimeUpdate(
21+
hass: HomeAssistant,
22+
after?: string,
23+
before?: string,
24+
weekdays?: string[]
25+
): number | undefined {
26+
const timezone =
27+
hass.locale.time_zone === TimeZone.server
28+
? hass.config.time_zone
29+
: Intl.DateTimeFormat().resolvedOptions().timeZone;
30+
31+
const now = new TZDate(new Date(), timezone);
32+
const updates: Date[] = [];
33+
34+
// Calculate next occurrence of after time
35+
if (after) {
36+
let afterDate = parseTimeString(after, timezone);
37+
if (afterDate <= now) {
38+
// If time has passed today, schedule for tomorrow
39+
afterDate = addDays(afterDate, 1);
40+
}
41+
updates.push(afterDate);
42+
}
43+
44+
// Calculate next occurrence of before time
45+
if (before) {
46+
let beforeDate = parseTimeString(before, timezone);
47+
if (beforeDate <= now) {
48+
// If time has passed today, schedule for tomorrow
49+
beforeDate = addDays(beforeDate, 1);
50+
}
51+
updates.push(beforeDate);
52+
}
53+
54+
// If weekdays are specified, check for midnight (weekday transition)
55+
if (weekdays && weekdays.length > 0 && weekdays.length < 7) {
56+
// Calculate next midnight using startOfDay + addDays
57+
const tomorrow = addDays(now, 1);
58+
const midnight = startOfDay(tomorrow);
59+
updates.push(midnight);
60+
}
61+
62+
if (updates.length === 0) {
63+
return undefined;
64+
}
65+
66+
// Find the soonest update time
67+
const nextUpdate = updates.reduce((soonest, current) =>
68+
current < soonest ? current : soonest
69+
);
70+
71+
// Add 1 minute buffer to ensure we're past the boundary
72+
const updateWithBuffer = addMinutes(nextUpdate, 1);
73+
74+
// Calculate difference in milliseconds
75+
return differenceInMilliseconds(updateWithBuffer, now);
76+
}

src/common/datetime/check_time.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { WEEKDAY_MAP, type WeekdayShort } from "./weekday";
1010
* @param timezone The timezone to use
1111
* @returns The Date object
1212
*/
13-
const parseTimeString = (timeString: string, timezone: string): Date => {
13+
export const parseTimeString = (timeString: string, timezone: string): Date => {
1414
const parts = timeString.split(":");
1515

1616
if (parts.length < 2 || parts.length > 3) {

src/mixins/conditional-listener-mixin.ts

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,7 @@
11
import type { ReactiveElement } from "lit";
2-
import { listenMediaQuery } from "../common/dom/media_query";
3-
import type { HomeAssistant } from "../types";
4-
import type { Condition } from "../panels/lovelace/common/validate-condition";
5-
import { checkConditionsMet } from "../panels/lovelace/common/validate-condition";
62

73
type Constructor<T> = abstract new (...args: any[]) => T;
84

9-
/**
10-
* Extract media queries from conditions recursively
11-
*/
12-
export function extractMediaQueries(conditions: Condition[]): string[] {
13-
return conditions.reduce<string[]>((array, c) => {
14-
if ("conditions" in c && c.conditions) {
15-
array.push(...extractMediaQueries(c.conditions));
16-
}
17-
if (c.condition === "screen" && c.media_query) {
18-
array.push(c.media_query);
19-
}
20-
return array;
21-
}, []);
22-
}
23-
24-
/**
25-
* Helper to setup media query listeners for conditional visibility
26-
*/
27-
export function setupMediaQueryListeners(
28-
conditions: Condition[],
29-
hass: HomeAssistant,
30-
addListener: (unsub: () => void) => void,
31-
onUpdate: (conditionsMet: boolean) => void
32-
): void {
33-
const mediaQueries = extractMediaQueries(conditions);
34-
35-
if (mediaQueries.length === 0) return;
36-
37-
// Optimization for single media query
38-
const hasOnlyMediaQuery =
39-
conditions.length === 1 &&
40-
conditions[0].condition === "screen" &&
41-
!!conditions[0].media_query;
42-
43-
mediaQueries.forEach((mediaQuery) => {
44-
const unsub = listenMediaQuery(mediaQuery, (matches) => {
45-
if (hasOnlyMediaQuery) {
46-
onUpdate(matches);
47-
} else {
48-
const conditionsMet = checkConditionsMet(conditions, hass);
49-
onUpdate(conditionsMet);
50-
}
51-
});
52-
addListener(unsub);
53-
});
54-
}
55-
565
/**
576
* Mixin to handle conditional listeners for visibility control
587
*

src/panels/lovelace/badges/hui-badge.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@ import { fireEvent } from "../../../common/dom/fire_event";
55
import "../../../components/ha-svg-icon";
66
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
77
import type { HomeAssistant } from "../../../types";
8-
import {
9-
ConditionalListenerMixin,
10-
setupMediaQueryListeners,
11-
} from "../../../mixins/conditional-listener-mixin";
8+
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
9+
import { setupMediaQueryListeners } from "../../../common/condition/listeners";
1210
import { checkConditionsMet } from "../common/validate-condition";
1311
import { createBadgeElement } from "../create-element/create-badge-element";
1412
import { createErrorBadgeConfig } from "../create-element/create-element-base";

src/panels/lovelace/cards/hui-card.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@ import { fireEvent } from "../../../common/dom/fire_event";
55
import "../../../components/ha-svg-icon";
66
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
77
import type { HomeAssistant } from "../../../types";
8-
import {
9-
ConditionalListenerMixin,
10-
setupMediaQueryListeners,
11-
} from "../../../mixins/conditional-listener-mixin";
8+
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
9+
import { setupMediaQueryListeners } from "../../../common/condition/listeners";
1210
import { migrateLayoutToGridOptions } from "../common/compute-card-grid-size";
1311
import { computeCardSize } from "../common/compute-card-size";
1412
import { checkConditionsMet } from "../common/validate-condition";

src/panels/lovelace/components/hui-conditional-base.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ import type { PropertyValues } from "lit";
22
import { ReactiveElement } from "lit";
33
import { customElement, property, state } from "lit/decorators";
44
import type { HomeAssistant } from "../../../types";
5-
import {
6-
ConditionalListenerMixin,
7-
setupMediaQueryListeners,
8-
} from "../../../mixins/conditional-listener-mixin";
5+
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
6+
import { setupMediaQueryListeners } from "../../../common/condition/listeners";
97
import type { HuiCard } from "../cards/hui-card";
108
import type { ConditionalCardConfig } from "../cards/types";
119
import type { Condition } from "../common/validate-condition";

src/panels/lovelace/heading-badges/hui-heading-badge.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@ import { customElement, property } from "lit/decorators";
44
import { fireEvent } from "../../../common/dom/fire_event";
55
import "../../../components/ha-svg-icon";
66
import type { HomeAssistant } from "../../../types";
7-
import {
8-
ConditionalListenerMixin,
9-
setupMediaQueryListeners,
10-
} from "../../../mixins/conditional-listener-mixin";
7+
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
8+
import { setupMediaQueryListeners } from "../../../common/condition/listeners";
119
import { checkConditionsMet } from "../common/validate-condition";
1210
import { createHeadingBadgeElement } from "../create-element/create-heading-badge-element";
1311
import type { LovelaceHeadingBadge } from "../types";

src/panels/lovelace/sections/hui-section.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,8 @@ import type {
1313
} from "../../../data/lovelace/config/section";
1414
import { isStrategySection } from "../../../data/lovelace/config/section";
1515
import type { HomeAssistant } from "../../../types";
16-
import {
17-
ConditionalListenerMixin,
18-
setupMediaQueryListeners,
19-
} from "../../../mixins/conditional-listener-mixin";
16+
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
17+
import { setupMediaQueryListeners } from "../../../common/condition/listeners";
2018
import "../cards/hui-card";
2119
import type { HuiCard } from "../cards/hui-card";
2220
import { checkConditionsMet } from "../common/validate-condition";

0 commit comments

Comments
 (0)