mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-13 21:10:37 +00:00
Compare commits
1 Commits
sec_pypi_p
...
renovate/n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6776336af |
13
.github/workflows/release.yaml
vendored
13
.github/workflows/release.yaml
vendored
@@ -19,11 +19,8 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
name: Release
|
name: Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: pypi
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write # Required to upload release assets
|
contents: write # Required to upload release assets
|
||||||
id-token: write # For "Trusted Publisher" to PyPi
|
|
||||||
if: github.repository_owner == 'home-assistant'
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
@@ -49,18 +46,14 @@ jobs:
|
|||||||
run: ./script/translations_download
|
run: ./script/translations_download
|
||||||
env:
|
env:
|
||||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
||||||
|
|
||||||
- name: Build and release package
|
- name: Build and release package
|
||||||
run: |
|
run: |
|
||||||
python3 -m pip install build
|
python3 -m pip install twine build
|
||||||
|
export TWINE_USERNAME="__token__"
|
||||||
|
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
||||||
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
|
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
|
||||||
script/release
|
script/release
|
||||||
|
|
||||||
- name: Publish to PyPI
|
|
||||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
|
||||||
with:
|
|
||||||
skip-existing: true
|
|
||||||
|
|
||||||
- name: Upload release assets
|
- name: Upload release assets
|
||||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
---
|
|
||||||
title: Dropdown
|
|
||||||
---
|
|
||||||
|
|
||||||
# Dropdown `<ha-dropdown>`
|
|
||||||
|
|
||||||
## Implementation
|
|
||||||
|
|
||||||
A compact, accessible dropdown menu for choosing actions or settings. `ha-dropdown` supports composed menu items (`<ha-dropdown-item>`) for icons, submenus, checkboxes, disabled entries, and destructive variants. Use composition with `slot="trigger"` to control the trigger button and use `<ha-dropdown-item>` for rich item content.
|
|
||||||
|
|
||||||
### Example usage (composition)
|
|
||||||
|
|
||||||
```html
|
|
||||||
<ha-dropdown open>
|
|
||||||
<ha-button slot="trigger" with-caret>Dropdown</ha-button>
|
|
||||||
|
|
||||||
<ha-dropdown-item>
|
|
||||||
<ha-svg-icon .path="mdiContentCut" slot="icon"></ha-svg-icon>
|
|
||||||
Cut
|
|
||||||
</ha-dropdown-item>
|
|
||||||
|
|
||||||
<ha-dropdown-item>
|
|
||||||
<ha-svg-icon .path="mdiContentCopy" slot="icon"></ha-svg-icon>
|
|
||||||
Copy
|
|
||||||
</ha-dropdown-item>
|
|
||||||
|
|
||||||
<ha-dropdown-item disabled>
|
|
||||||
<ha-svg-icon .path="mdiContentPaste" slot="icon"></ha-svg-icon>
|
|
||||||
Paste
|
|
||||||
</ha-dropdown-item>
|
|
||||||
|
|
||||||
<ha-dropdown-item>
|
|
||||||
Show images
|
|
||||||
<ha-dropdown-item slot="submenu" value="show-all-images"
|
|
||||||
>Show all images</ha-dropdown-item
|
|
||||||
>
|
|
||||||
<ha-dropdown-item slot="submenu" value="show-thumbnails"
|
|
||||||
>Show thumbnails</ha-dropdown-item
|
|
||||||
>
|
|
||||||
</ha-dropdown-item>
|
|
||||||
|
|
||||||
<ha-dropdown-item type="checkbox" checked>Emoji shortcuts</ha-dropdown-item>
|
|
||||||
<ha-dropdown-item type="checkbox" checked>Word wrap</ha-dropdown-item>
|
|
||||||
|
|
||||||
<ha-dropdown-item variant="danger">
|
|
||||||
<ha-svg-icon .path="mdiDelete" slot="icon"></ha-svg-icon>
|
|
||||||
Delete
|
|
||||||
</ha-dropdown-item>
|
|
||||||
</ha-dropdown>
|
|
||||||
```
|
|
||||||
|
|
||||||
### API
|
|
||||||
|
|
||||||
This component is based on the webawesome dropdown component.
|
|
||||||
Check the [webawesome documentation](https://webawesome.com/docs/components/dropdown/) for more details.
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
import "@home-assistant/webawesome/dist/components/button/button";
|
|
||||||
import "@home-assistant/webawesome/dist/components/dropdown/dropdown";
|
|
||||||
import "@home-assistant/webawesome/dist/components/icon/icon";
|
|
||||||
import "@home-assistant/webawesome/dist/components/popup/popup";
|
|
||||||
import {
|
|
||||||
mdiContentCopy,
|
|
||||||
mdiContentCut,
|
|
||||||
mdiContentPaste,
|
|
||||||
mdiDelete,
|
|
||||||
} from "@mdi/js";
|
|
||||||
import type { TemplateResult } from "lit";
|
|
||||||
import { css, html, LitElement } from "lit";
|
|
||||||
import { customElement } from "lit/decorators";
|
|
||||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
|
||||||
import "../../../../src/components/ha-button";
|
|
||||||
import "../../../../src/components/ha-card";
|
|
||||||
import "../../../../src/components/ha-dropdown";
|
|
||||||
import "../../../../src/components/ha-dropdown-item";
|
|
||||||
import "../../../../src/components/ha-icon-button";
|
|
||||||
import "../../../../src/components/ha-svg-icon";
|
|
||||||
|
|
||||||
@customElement("demo-components-ha-dropdown")
|
|
||||||
export class DemoHaDropdown extends LitElement {
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
return html`
|
|
||||||
${["light", "dark"].map(
|
|
||||||
(mode) => html`
|
|
||||||
<div class=${mode}>
|
|
||||||
<ha-card header="ha-button in ${mode}">
|
|
||||||
<div class="card-content">
|
|
||||||
<ha-dropdown open>
|
|
||||||
<ha-button slot="trigger" with-caret>Dropdown</ha-button>
|
|
||||||
|
|
||||||
<ha-dropdown-item>
|
|
||||||
<ha-svg-icon
|
|
||||||
.path=${mdiContentCut}
|
|
||||||
slot="icon"
|
|
||||||
></ha-svg-icon>
|
|
||||||
Cut
|
|
||||||
</ha-dropdown-item>
|
|
||||||
<ha-dropdown-item>
|
|
||||||
<ha-svg-icon
|
|
||||||
.path=${mdiContentCopy}
|
|
||||||
slot="icon"
|
|
||||||
></ha-svg-icon>
|
|
||||||
Copy
|
|
||||||
</ha-dropdown-item>
|
|
||||||
<ha-dropdown-item disabled>
|
|
||||||
<ha-svg-icon
|
|
||||||
.path=${mdiContentPaste}
|
|
||||||
slot="icon"
|
|
||||||
></ha-svg-icon>
|
|
||||||
Paste
|
|
||||||
</ha-dropdown-item>
|
|
||||||
<ha-dropdown-item>
|
|
||||||
Show images
|
|
||||||
<ha-dropdown-item slot="submenu" value="show-all-images"
|
|
||||||
>Show All Images</ha-dropdown-item
|
|
||||||
>
|
|
||||||
<ha-dropdown-item slot="submenu" value="show-thumbnails"
|
|
||||||
>Show Thumbnails</ha-dropdown-item
|
|
||||||
>
|
|
||||||
</ha-dropdown-item>
|
|
||||||
<ha-dropdown-item type="checkbox" checked
|
|
||||||
>Emoji Shortcuts</ha-dropdown-item
|
|
||||||
>
|
|
||||||
<ha-dropdown-item type="checkbox" checked
|
|
||||||
>Word Wrap</ha-dropdown-item
|
|
||||||
>
|
|
||||||
<ha-dropdown-item variant="danger">
|
|
||||||
<ha-svg-icon .path=${mdiDelete} slot="icon"></ha-svg-icon>
|
|
||||||
Delete
|
|
||||||
</ha-dropdown-item>
|
|
||||||
</ha-dropdown>
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
firstUpdated(changedProps) {
|
|
||||||
super.firstUpdated(changedProps);
|
|
||||||
applyThemesOnElement(
|
|
||||||
this.shadowRoot!.querySelector(".dark"),
|
|
||||||
{
|
|
||||||
default_theme: "default",
|
|
||||||
default_dark_theme: "default",
|
|
||||||
themes: {},
|
|
||||||
darkMode: true,
|
|
||||||
theme: "default",
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = css`
|
|
||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.dark,
|
|
||||||
.light {
|
|
||||||
display: block;
|
|
||||||
background-color: var(--primary-background-color);
|
|
||||||
padding: 0 50px;
|
|
||||||
}
|
|
||||||
.button {
|
|
||||||
padding: unset;
|
|
||||||
}
|
|
||||||
ha-card {
|
|
||||||
margin: 24px auto;
|
|
||||||
}
|
|
||||||
.card-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
.card-content div {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"demo-components-ha-dropdown": DemoHaDropdown;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
"@fullcalendar/list": "6.1.19",
|
"@fullcalendar/list": "6.1.19",
|
||||||
"@fullcalendar/luxon3": "6.1.19",
|
"@fullcalendar/luxon3": "6.1.19",
|
||||||
"@fullcalendar/timegrid": "6.1.19",
|
"@fullcalendar/timegrid": "6.1.19",
|
||||||
"@home-assistant/webawesome": "3.0.0",
|
"@home-assistant/webawesome": "3.0.0-beta.6.ha.7",
|
||||||
"@lezer/highlight": "1.2.3",
|
"@lezer/highlight": "1.2.3",
|
||||||
"@lit-labs/motion": "1.0.9",
|
"@lit-labs/motion": "1.0.9",
|
||||||
"@lit-labs/observers": "2.0.6",
|
"@lit-labs/observers": "2.0.6",
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
"lit": "3.3.1",
|
"lit": "3.3.1",
|
||||||
"lit-html": "3.3.1",
|
"lit-html": "3.3.1",
|
||||||
"luxon": "3.7.2",
|
"luxon": "3.7.2",
|
||||||
"marked": "17.0.0",
|
"marked": "16.4.2",
|
||||||
"memoize-one": "6.0.0",
|
"memoize-one": "6.0.0",
|
||||||
"node-vibrant": "4.0.3",
|
"node-vibrant": "4.0.3",
|
||||||
"object-hash": "3.0.0",
|
"object-hash": "3.0.0",
|
||||||
@@ -237,6 +237,6 @@
|
|||||||
},
|
},
|
||||||
"packageManager": "yarn@4.10.3",
|
"packageManager": "yarn@4.10.3",
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.21.1"
|
"node": "24.11.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
# Pushes a new version to PyPi.
|
||||||
|
|
||||||
# Stop on errors
|
# Stop on errors
|
||||||
set -e
|
set -e
|
||||||
@@ -11,4 +12,5 @@ yarn install
|
|||||||
script/build_frontend
|
script/build_frontend
|
||||||
|
|
||||||
rm -rf dist home_assistant_frontend.egg-info
|
rm -rf dist home_assistant_frontend.egg-info
|
||||||
python3 -m build -q
|
python3 -m build
|
||||||
|
python3 -m twine upload dist/*.whl --skip-existing
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import type {
|
|
||||||
Condition,
|
|
||||||
TimeCondition,
|
|
||||||
} from "../../panels/lovelace/common/validate-condition";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract media queries from conditions recursively
|
|
||||||
*/
|
|
||||||
export function extractMediaQueries(conditions: Condition[]): string[] {
|
|
||||||
return conditions.reduce<string[]>((array, c) => {
|
|
||||||
if ("conditions" in c && c.conditions) {
|
|
||||||
array.push(...extractMediaQueries(c.conditions));
|
|
||||||
}
|
|
||||||
if (c.condition === "screen" && c.media_query) {
|
|
||||||
array.push(c.media_query);
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract time conditions from conditions recursively
|
|
||||||
*/
|
|
||||||
export function extractTimeConditions(
|
|
||||||
conditions: Condition[]
|
|
||||||
): TimeCondition[] {
|
|
||||||
return conditions.reduce<TimeCondition[]>((array, c) => {
|
|
||||||
if ("conditions" in c && c.conditions) {
|
|
||||||
array.push(...extractTimeConditions(c.conditions));
|
|
||||||
}
|
|
||||||
if (c.condition === "time") {
|
|
||||||
array.push(c);
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import { listenMediaQuery } from "../dom/media_query";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
|
||||||
import type { Condition } from "../../panels/lovelace/common/validate-condition";
|
|
||||||
import { checkConditionsMet } from "../../panels/lovelace/common/validate-condition";
|
|
||||||
import { extractMediaQueries, extractTimeConditions } from "./extract";
|
|
||||||
import { calculateNextTimeUpdate } from "./time-calculator";
|
|
||||||
|
|
||||||
/** Maximum delay for setTimeout (2^31 - 1 milliseconds, ~24.8 days)
|
|
||||||
* Values exceeding this will overflow and execute immediately
|
|
||||||
*
|
|
||||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#maximum_delay_value
|
|
||||||
*/
|
|
||||||
const MAX_TIMEOUT_DELAY = 2147483647;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to setup media query listeners for conditional visibility
|
|
||||||
*/
|
|
||||||
export function setupMediaQueryListeners(
|
|
||||||
conditions: Condition[],
|
|
||||||
hass: HomeAssistant,
|
|
||||||
addListener: (unsub: () => void) => void,
|
|
||||||
onUpdate: (conditionsMet: boolean) => void
|
|
||||||
): void {
|
|
||||||
const mediaQueries = extractMediaQueries(conditions);
|
|
||||||
|
|
||||||
if (mediaQueries.length === 0) return;
|
|
||||||
|
|
||||||
// Optimization for single media query
|
|
||||||
const hasOnlyMediaQuery =
|
|
||||||
conditions.length === 1 &&
|
|
||||||
conditions[0].condition === "screen" &&
|
|
||||||
!!conditions[0].media_query;
|
|
||||||
|
|
||||||
mediaQueries.forEach((mediaQuery) => {
|
|
||||||
const unsub = listenMediaQuery(mediaQuery, (matches) => {
|
|
||||||
if (hasOnlyMediaQuery) {
|
|
||||||
onUpdate(matches);
|
|
||||||
} else {
|
|
||||||
const conditionsMet = checkConditionsMet(conditions, hass);
|
|
||||||
onUpdate(conditionsMet);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
addListener(unsub);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to setup time-based listeners for conditional visibility
|
|
||||||
*/
|
|
||||||
export function setupTimeListeners(
|
|
||||||
conditions: Condition[],
|
|
||||||
hass: HomeAssistant,
|
|
||||||
addListener: (unsub: () => void) => void,
|
|
||||||
onUpdate: (conditionsMet: boolean) => void
|
|
||||||
): void {
|
|
||||||
const timeConditions = extractTimeConditions(conditions);
|
|
||||||
|
|
||||||
if (timeConditions.length === 0) return;
|
|
||||||
|
|
||||||
timeConditions.forEach((timeCondition) => {
|
|
||||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
||||||
|
|
||||||
const scheduleUpdate = () => {
|
|
||||||
const delay = calculateNextTimeUpdate(hass, timeCondition);
|
|
||||||
|
|
||||||
if (delay === undefined) return;
|
|
||||||
|
|
||||||
// Cap delay to prevent setTimeout overflow
|
|
||||||
const cappedDelay = Math.min(delay, MAX_TIMEOUT_DELAY);
|
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
if (delay <= MAX_TIMEOUT_DELAY) {
|
|
||||||
const conditionsMet = checkConditionsMet(conditions, hass);
|
|
||||||
onUpdate(conditionsMet);
|
|
||||||
}
|
|
||||||
scheduleUpdate();
|
|
||||||
}, cappedDelay);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Register cleanup function once, outside of scheduleUpdate
|
|
||||||
addListener(() => {
|
|
||||||
if (timeoutId !== undefined) {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
scheduleUpdate();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import { TZDate } from "@date-fns/tz";
|
|
||||||
import {
|
|
||||||
startOfDay,
|
|
||||||
addDays,
|
|
||||||
addMinutes,
|
|
||||||
differenceInMilliseconds,
|
|
||||||
} from "date-fns";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
|
||||||
import { TimeZone } from "../../data/translation";
|
|
||||||
import { parseTimeString } from "../datetime/check_time";
|
|
||||||
import type { TimeCondition } from "../../panels/lovelace/common/validate-condition";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate milliseconds until next time boundary for a time condition
|
|
||||||
* @param hass Home Assistant object
|
|
||||||
* @param timeCondition Time condition to calculate next update for
|
|
||||||
* @returns Milliseconds until next boundary, or undefined if no boundaries
|
|
||||||
*/
|
|
||||||
export function calculateNextTimeUpdate(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
{ after, before, weekdays }: Omit<TimeCondition, "condition">
|
|
||||||
): number | undefined {
|
|
||||||
const timezone =
|
|
||||||
hass.locale.time_zone === TimeZone.server
|
|
||||||
? hass.config.time_zone
|
|
||||||
: Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
||||||
|
|
||||||
const now = new TZDate(new Date(), timezone);
|
|
||||||
const updates: Date[] = [];
|
|
||||||
|
|
||||||
// Calculate next occurrence of after time
|
|
||||||
if (after) {
|
|
||||||
let afterDate = parseTimeString(after, timezone);
|
|
||||||
if (afterDate <= now) {
|
|
||||||
// If time has passed today, schedule for tomorrow
|
|
||||||
afterDate = addDays(afterDate, 1);
|
|
||||||
}
|
|
||||||
updates.push(afterDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate next occurrence of before time
|
|
||||||
if (before) {
|
|
||||||
let beforeDate = parseTimeString(before, timezone);
|
|
||||||
if (beforeDate <= now) {
|
|
||||||
// If time has passed today, schedule for tomorrow
|
|
||||||
beforeDate = addDays(beforeDate, 1);
|
|
||||||
}
|
|
||||||
updates.push(beforeDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If weekdays are specified, check for midnight (weekday transition)
|
|
||||||
if (weekdays && weekdays.length > 0 && weekdays.length < 7) {
|
|
||||||
// Calculate next midnight using startOfDay + addDays
|
|
||||||
const tomorrow = addDays(now, 1);
|
|
||||||
const midnight = startOfDay(tomorrow);
|
|
||||||
updates.push(midnight);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updates.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the soonest update time
|
|
||||||
const nextUpdate = updates.reduce((soonest, current) =>
|
|
||||||
current < soonest ? current : soonest
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add 1 minute buffer to ensure we're past the boundary
|
|
||||||
const updateWithBuffer = addMinutes(nextUpdate, 1);
|
|
||||||
|
|
||||||
// Calculate difference in milliseconds
|
|
||||||
return differenceInMilliseconds(updateWithBuffer, now);
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
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;
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,18 @@
|
|||||||
import { getWeekStartByLocale } from "weekstart";
|
import { getWeekStartByLocale } from "weekstart";
|
||||||
import type { FrontendLocaleData } from "../../data/translation";
|
import type { FrontendLocaleData } from "../../data/translation";
|
||||||
import { FirstWeekday } from "../../data/translation";
|
import { FirstWeekday } from "../../data/translation";
|
||||||
import { WEEKDAYS_LONG, type WeekdayIndex } from "./weekday";
|
|
||||||
|
export const weekdays = [
|
||||||
|
"sunday",
|
||||||
|
"monday",
|
||||||
|
"tuesday",
|
||||||
|
"wednesday",
|
||||||
|
"thursday",
|
||||||
|
"friday",
|
||||||
|
"saturday",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type WeekdayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
|
|
||||||
export const firstWeekdayIndex = (locale: FrontendLocaleData): WeekdayIndex => {
|
export const firstWeekdayIndex = (locale: FrontendLocaleData): WeekdayIndex => {
|
||||||
if (locale.first_weekday === FirstWeekday.language) {
|
if (locale.first_weekday === FirstWeekday.language) {
|
||||||
@@ -12,12 +23,12 @@ export const firstWeekdayIndex = (locale: FrontendLocaleData): WeekdayIndex => {
|
|||||||
}
|
}
|
||||||
return (getWeekStartByLocale(locale.language) % 7) as WeekdayIndex;
|
return (getWeekStartByLocale(locale.language) % 7) as WeekdayIndex;
|
||||||
}
|
}
|
||||||
return WEEKDAYS_LONG.includes(locale.first_weekday)
|
return weekdays.includes(locale.first_weekday)
|
||||||
? (WEEKDAYS_LONG.indexOf(locale.first_weekday) as WeekdayIndex)
|
? (weekdays.indexOf(locale.first_weekday) as WeekdayIndex)
|
||||||
: 1;
|
: 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const firstWeekday = (locale: FrontendLocaleData) => {
|
export const firstWeekday = (locale: FrontendLocaleData) => {
|
||||||
const index = firstWeekdayIndex(locale);
|
const index = firstWeekdayIndex(locale);
|
||||||
return WEEKDAYS_LONG[index];
|
return weekdays[index];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
export type WeekdayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
||||||
|
|
||||||
export type WeekdayShort =
|
|
||||||
| "sun"
|
|
||||||
| "mon"
|
|
||||||
| "tue"
|
|
||||||
| "wed"
|
|
||||||
| "thu"
|
|
||||||
| "fri"
|
|
||||||
| "sat";
|
|
||||||
|
|
||||||
export type WeekdayLong =
|
|
||||||
| "sunday"
|
|
||||||
| "monday"
|
|
||||||
| "tuesday"
|
|
||||||
| "wednesday"
|
|
||||||
| "thursday"
|
|
||||||
| "friday"
|
|
||||||
| "saturday";
|
|
||||||
|
|
||||||
export const WEEKDAYS_SHORT = [
|
|
||||||
"sun",
|
|
||||||
"mon",
|
|
||||||
"tue",
|
|
||||||
"wed",
|
|
||||||
"thu",
|
|
||||||
"fri",
|
|
||||||
"sat",
|
|
||||||
] as const satisfies readonly WeekdayShort[];
|
|
||||||
|
|
||||||
export const WEEKDAYS_LONG = [
|
|
||||||
"sunday",
|
|
||||||
"monday",
|
|
||||||
"tuesday",
|
|
||||||
"wednesday",
|
|
||||||
"thursday",
|
|
||||||
"friday",
|
|
||||||
"saturday",
|
|
||||||
] as const satisfies readonly WeekdayLong[];
|
|
||||||
|
|
||||||
export const WEEKDAY_MAP = {
|
|
||||||
0: "sun",
|
|
||||||
1: "mon",
|
|
||||||
2: "tue",
|
|
||||||
3: "wed",
|
|
||||||
4: "thu",
|
|
||||||
5: "fri",
|
|
||||||
6: "sat",
|
|
||||||
} as const satisfies Record<WeekdayIndex, WeekdayShort>;
|
|
||||||
|
|
||||||
export const WEEKDAY_SHORT_TO_LONG = {
|
|
||||||
sun: "sunday",
|
|
||||||
mon: "monday",
|
|
||||||
tue: "tuesday",
|
|
||||||
wed: "wednesday",
|
|
||||||
thu: "thursday",
|
|
||||||
fri: "friday",
|
|
||||||
sat: "saturday",
|
|
||||||
} as const satisfies Record<WeekdayShort, WeekdayLong>;
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { ThemeVars } from "../../data/ws-themes";
|
import type { ThemeVars } from "../../data/ws-themes";
|
||||||
import { darkColorVariables } from "../../resources/theme/color";
|
import { darkColorVariables } from "../../resources/theme/color";
|
||||||
import { darkSemanticVariables } from "../../resources/theme/semantic.globals";
|
|
||||||
import { derivedStyles } from "../../resources/theme/theme";
|
import { derivedStyles } from "../../resources/theme/theme";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import {
|
import {
|
||||||
@@ -53,7 +52,7 @@ export const applyThemesOnElement = (
|
|||||||
|
|
||||||
if (themeToApply && darkMode) {
|
if (themeToApply && darkMode) {
|
||||||
cacheKey = `${cacheKey}__dark`;
|
cacheKey = `${cacheKey}__dark`;
|
||||||
themeRules = { ...darkSemanticVariables, ...darkColorVariables };
|
themeRules = { ...darkColorVariables };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (themeToApply === "default") {
|
if (themeToApply === "default") {
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
/**
|
|
||||||
* Parses a CSS duration string (e.g., "300ms", "3s") and returns the duration in milliseconds.
|
|
||||||
*
|
|
||||||
* @param duration - A CSS duration string (e.g., "300ms", "3s", "0.5s")
|
|
||||||
* @returns The duration in milliseconds, or 0 if the input is invalid
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* parseAnimationDuration("300ms") // Returns 300
|
|
||||||
* parseAnimationDuration("3s") // Returns 3000
|
|
||||||
* parseAnimationDuration("0.5s") // Returns 500
|
|
||||||
* parseAnimationDuration("invalid") // Returns 0
|
|
||||||
*/
|
|
||||||
export const parseAnimationDuration = (duration: string): number => {
|
|
||||||
const trimmed = duration.trim();
|
|
||||||
|
|
||||||
let value: number;
|
|
||||||
let multiplier: number;
|
|
||||||
|
|
||||||
if (trimmed.endsWith("ms")) {
|
|
||||||
value = parseFloat(trimmed.slice(0, -2));
|
|
||||||
multiplier = 1;
|
|
||||||
} else if (trimmed.endsWith("s")) {
|
|
||||||
value = parseFloat(trimmed.slice(0, -1));
|
|
||||||
multiplier = 1000;
|
|
||||||
} else {
|
|
||||||
// No recognized unit, try parsing as number (assume ms)
|
|
||||||
value = parseFloat(trimmed);
|
|
||||||
multiplier = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isFinite(value) || value < 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value * multiplier;
|
|
||||||
};
|
|
||||||
@@ -21,6 +21,7 @@ import "../ha-combo-box-item";
|
|||||||
import "../ha-generic-picker";
|
import "../ha-generic-picker";
|
||||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||||
import "../ha-icon-button";
|
import "../ha-icon-button";
|
||||||
|
import "../ha-input-helper-text";
|
||||||
import type {
|
import type {
|
||||||
PickerComboBoxItem,
|
PickerComboBoxItem,
|
||||||
PickerComboBoxSearchFn,
|
PickerComboBoxSearchFn,
|
||||||
@@ -474,7 +475,6 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
.hideClearIcon=${this.hideClearIcon}
|
.hideClearIcon=${this.hideClearIcon}
|
||||||
.searchFn=${this._searchFn}
|
.searchFn=${this._searchFn}
|
||||||
.valueRenderer=${this._valueRenderer}
|
.valueRenderer=${this._valueRenderer}
|
||||||
.helper=${this.helper}
|
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-generic-picker>
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import DropdownItem from "@home-assistant/webawesome/dist/components/dropdown-item/dropdown-item";
|
|
||||||
import { css, type CSSResultGroup } from "lit";
|
|
||||||
import { customElement } from "lit/decorators";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Home Assistant dropdown item component
|
|
||||||
*
|
|
||||||
* @element ha-dropdown-item
|
|
||||||
* @extends {DropdownItem}
|
|
||||||
*
|
|
||||||
* @summary
|
|
||||||
* A stylable dropdown item component supporting Home Assistant theming, variants, and appearances based on webawesome dropdown item.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@customElement("ha-dropdown-item")
|
|
||||||
export class HaDropdownItem extends DropdownItem {
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return [
|
|
||||||
DropdownItem.styles,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
min-height: var(--ha-space-10);
|
|
||||||
}
|
|
||||||
|
|
||||||
#icon ::slotted(*) {
|
|
||||||
color: var(--ha-color-on-neutral-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([variant="danger"]) #icon ::slotted(*) {
|
|
||||||
color: var(--ha-color-on-danger-quiet);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-dropdown-item": HaDropdownItem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import Dropdown from "@home-assistant/webawesome/dist/components/dropdown/dropdown";
|
|
||||||
import { css, type CSSResultGroup } from "lit";
|
|
||||||
import { customElement, property } from "lit/decorators";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Home Assistant dropdown component
|
|
||||||
*
|
|
||||||
* @element ha-dropdown
|
|
||||||
* @extends {Dropdown}
|
|
||||||
*
|
|
||||||
* @summary
|
|
||||||
* A stylable dropdown component supporting Home Assistant theming, variants, and appearances based on webawesome dropdown.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@customElement("ha-dropdown")
|
|
||||||
export class HaDropdown extends Dropdown {
|
|
||||||
@property({ attribute: false }) dropdownTag = "ha-dropdown";
|
|
||||||
|
|
||||||
@property({ attribute: false }) dropdownItemTag = "ha-dropdown-item";
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return [
|
|
||||||
Dropdown.styles,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
font-size: var(--ha-dropdown-font-size, var(--ha-font-size-m));
|
|
||||||
--wa-color-surface-raised: var(
|
|
||||||
--card-background-color,
|
|
||||||
var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu {
|
|
||||||
padding: var(--ha-space-1);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-dropdown": HaDropdown;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -712,7 +712,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
this._selectedSection = section as TargetTypeFloorless | undefined;
|
this._selectedSection = section as TargetTypeFloorless | undefined;
|
||||||
|
|
||||||
return this._getItemsMemoized(
|
return this._getItemsMemoized(
|
||||||
this.hass.localize,
|
|
||||||
this.entityFilter,
|
this.entityFilter,
|
||||||
this.deviceFilter,
|
this.deviceFilter,
|
||||||
this.includeDomains,
|
this.includeDomains,
|
||||||
@@ -726,7 +725,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
private _getItemsMemoized = memoizeOne(
|
private _getItemsMemoized = memoizeOne(
|
||||||
(
|
(
|
||||||
localize: HomeAssistant["localize"],
|
|
||||||
entityFilter: this["entityFilter"],
|
entityFilter: this["entityFilter"],
|
||||||
deviceFilter: this["deviceFilter"],
|
deviceFilter: this["deviceFilter"],
|
||||||
includeDomains: this["includeDomains"],
|
includeDomains: this["includeDomains"],
|
||||||
@@ -744,7 +742,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
)[] = [];
|
)[] = [];
|
||||||
|
|
||||||
if (!filterType || filterType === "entity") {
|
if (!filterType || filterType === "entity") {
|
||||||
let entityItems = this._getEntitiesMemoized(
|
let entities = this._getEntitiesMemoized(
|
||||||
this.hass,
|
this.hass,
|
||||||
includeDomains,
|
includeDomains,
|
||||||
undefined,
|
undefined,
|
||||||
@@ -760,25 +758,27 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
entityItems = this._filterGroup(
|
entities = this._filterGroup(
|
||||||
"entity",
|
"entity",
|
||||||
entityItems,
|
entities,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
(item: EntityComboBoxItem) =>
|
(item: EntityComboBoxItem) =>
|
||||||
item.stateObj?.entity_id === searchTerm
|
item.stateObj?.entity_id === searchTerm
|
||||||
) as EntityComboBoxItem[];
|
) as EntityComboBoxItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!filterType && entityItems.length) {
|
if (!filterType && entities.length) {
|
||||||
// show group title
|
// show group title
|
||||||
items.push(localize("ui.components.target-picker.type.entities"));
|
items.push(
|
||||||
|
this.hass.localize("ui.components.target-picker.type.entities")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
items.push(...entityItems);
|
items.push(...entities);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!filterType || filterType === "device") {
|
if (!filterType || filterType === "device") {
|
||||||
let deviceItems = this._getDevicesMemoized(
|
let devices = this._getDevicesMemoized(
|
||||||
this.hass,
|
this.hass,
|
||||||
configEntryLookup,
|
configEntryLookup,
|
||||||
includeDomains,
|
includeDomains,
|
||||||
@@ -794,15 +794,17 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
deviceItems = this._filterGroup("device", deviceItems, searchTerm);
|
devices = this._filterGroup("device", devices, searchTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!filterType && deviceItems.length) {
|
if (!filterType && devices.length) {
|
||||||
// show group title
|
// show group title
|
||||||
items.push(localize("ui.components.target-picker.type.devices"));
|
items.push(
|
||||||
|
this.hass.localize("ui.components.target-picker.type.devices")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
items.push(...deviceItems);
|
items.push(...devices);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!filterType || filterType === "area") {
|
if (!filterType || filterType === "area") {
|
||||||
@@ -834,7 +836,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
if (!filterType && areasAndFloors.length) {
|
if (!filterType && areasAndFloors.length) {
|
||||||
// show group title
|
// show group title
|
||||||
items.push(localize("ui.components.target-picker.type.areas"));
|
items.push(
|
||||||
|
this.hass.localize("ui.components.target-picker.type.areas")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
items.push(
|
items.push(
|
||||||
@@ -875,7 +879,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
if (!filterType && labels.length) {
|
if (!filterType && labels.length) {
|
||||||
// show group title
|
// show group title
|
||||||
items.push(localize("ui.components.target-picker.type.labels"));
|
items.push(
|
||||||
|
this.hass.localize("ui.components.target-picker.type.labels")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
items.push(...labels);
|
items.push(...labels);
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { CONDITION_BUILDING_BLOCKS } from "./condition";
|
|||||||
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||||
import type { Action, Field, MODES } from "./script";
|
import type { Action, Field, MODES } from "./script";
|
||||||
import { migrateAutomationAction } from "./script";
|
import { migrateAutomationAction } from "./script";
|
||||||
import type { WeekdayShort } from "../common/datetime/weekday";
|
|
||||||
|
|
||||||
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
|
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
|
||||||
export const AUTOMATION_DEFAULT_MAX = 10;
|
export const AUTOMATION_DEFAULT_MAX = 10;
|
||||||
@@ -258,11 +257,13 @@ export interface ZoneCondition extends BaseCondition {
|
|||||||
zone: string;
|
zone: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Weekday = "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat";
|
||||||
|
|
||||||
export interface TimeCondition extends BaseCondition {
|
export interface TimeCondition extends BaseCondition {
|
||||||
condition: "time";
|
condition: "time";
|
||||||
after?: string;
|
after?: string;
|
||||||
before?: string;
|
before?: string;
|
||||||
weekday?: WeekdayShort | WeekdayShort[];
|
weekday?: Weekday | Weekday[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TemplateCondition extends BaseCondition {
|
export interface TemplateCondition extends BaseCondition {
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ export type EnergySolarForecasts = Record<string, EnergySolarForecast>;
|
|||||||
export interface DeviceConsumptionEnergyPreference {
|
export interface DeviceConsumptionEnergyPreference {
|
||||||
// This is an ever increasing value
|
// This is an ever increasing value
|
||||||
stat_consumption: string;
|
stat_consumption: string;
|
||||||
stat_rate?: string;
|
|
||||||
name?: string;
|
name?: string;
|
||||||
included_in_stat?: string;
|
included_in_stat?: string;
|
||||||
}
|
}
|
||||||
@@ -131,17 +130,11 @@ export interface FlowToGridSourceEnergyPreference {
|
|||||||
number_energy_price: number | null;
|
number_energy_price: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GridPowerSourceEnergyPreference {
|
|
||||||
// W meter
|
|
||||||
stat_rate: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GridSourceTypeEnergyPreference {
|
export interface GridSourceTypeEnergyPreference {
|
||||||
type: "grid";
|
type: "grid";
|
||||||
|
|
||||||
flow_from: FlowFromGridSourceEnergyPreference[];
|
flow_from: FlowFromGridSourceEnergyPreference[];
|
||||||
flow_to: FlowToGridSourceEnergyPreference[];
|
flow_to: FlowToGridSourceEnergyPreference[];
|
||||||
power?: GridPowerSourceEnergyPreference[];
|
|
||||||
|
|
||||||
cost_adjustment_day: number;
|
cost_adjustment_day: number;
|
||||||
}
|
}
|
||||||
@@ -150,7 +143,6 @@ export interface SolarSourceTypeEnergyPreference {
|
|||||||
type: "solar";
|
type: "solar";
|
||||||
|
|
||||||
stat_energy_from: string;
|
stat_energy_from: string;
|
||||||
stat_rate?: string;
|
|
||||||
config_entry_solar_forecast: string[] | null;
|
config_entry_solar_forecast: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +150,6 @@ export interface BatterySourceTypeEnergyPreference {
|
|||||||
type: "battery";
|
type: "battery";
|
||||||
stat_energy_from: string;
|
stat_energy_from: string;
|
||||||
stat_energy_to: string;
|
stat_energy_to: string;
|
||||||
stat_rate?: string;
|
|
||||||
}
|
}
|
||||||
export interface GasSourceTypeEnergyPreference {
|
export interface GasSourceTypeEnergyPreference {
|
||||||
type: "gas";
|
type: "gas";
|
||||||
|
|||||||
@@ -20,21 +20,6 @@
|
|||||||
<meta name="color-scheme" content="dark light" />
|
<meta name="color-scheme" content="dark light" />
|
||||||
<%= renderTemplate("_style_base.html.template") %>
|
<%= renderTemplate("_style_base.html.template") %>
|
||||||
<style>
|
<style>
|
||||||
@keyframes fade-out {
|
|
||||||
from {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
::view-transition-group(launch-screen) {
|
|
||||||
animation-duration: var(--ha-animation-base-duration, 350ms);
|
|
||||||
animation-timing-function: ease-out;
|
|
||||||
}
|
|
||||||
::view-transition-old(launch-screen) {
|
|
||||||
animation: fade-out var(--ha-animation-base-duration, 350ms) ease-out;
|
|
||||||
}
|
|
||||||
html {
|
html {
|
||||||
background-color: var(--primary-background-color, #fafafa);
|
background-color: var(--primary-background-color, #fafafa);
|
||||||
color: var(--primary-text-color, #212121);
|
color: var(--primary-text-color, #212121);
|
||||||
@@ -47,29 +32,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#ha-launch-screen {
|
#ha-launch-screen {
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
view-transition-name: launch-screen;
|
|
||||||
background-color: var(--primary-background-color, #fafafa);
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
/* body selector to avoid minification causing bad jinja2 */
|
|
||||||
body #ha-launch-screen {
|
|
||||||
background-color: var(--primary-background-color, #111111);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#ha-launch-screen.removing {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
}
|
||||||
#ha-launch-screen svg {
|
#ha-launch-screen svg {
|
||||||
width: 112px;
|
width: 112px;
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ const COMPONENTS = {
|
|||||||
light: () => import("../panels/light/ha-panel-light"),
|
light: () => import("../panels/light/ha-panel-light"),
|
||||||
security: () => import("../panels/security/ha-panel-security"),
|
security: () => import("../panels/security/ha-panel-security"),
|
||||||
climate: () => import("../panels/climate/ha-panel-climate"),
|
climate: () => import("../panels/climate/ha-panel-climate"),
|
||||||
home: () => import("../panels/home/ha-panel-home"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@customElement("partial-panel-resolver")
|
@customElement("partial-panel-resolver")
|
||||||
|
|||||||
@@ -1,56 +1,82 @@
|
|||||||
import type { ReactiveElement } from "lit";
|
import type { ReactiveElement } from "lit";
|
||||||
|
import { listenMediaQuery } from "../common/dom/media_query";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import {
|
|
||||||
setupMediaQueryListeners,
|
|
||||||
setupTimeListeners,
|
|
||||||
} from "../common/condition/listeners";
|
|
||||||
import type { Condition } from "../panels/lovelace/common/validate-condition";
|
import type { Condition } from "../panels/lovelace/common/validate-condition";
|
||||||
|
import { checkConditionsMet } from "../panels/lovelace/common/validate-condition";
|
||||||
|
|
||||||
type Constructor<T> = abstract new (...args: any[]) => T;
|
type Constructor<T> = abstract new (...args: any[]) => T;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base config type that can be used with conditional listeners
|
* Extract media queries from conditions recursively
|
||||||
*/
|
*/
|
||||||
export interface ConditionalConfig {
|
export function extractMediaQueries(conditions: Condition[]): string[] {
|
||||||
visibility?: Condition[];
|
return conditions.reduce<string[]>((array, c) => {
|
||||||
[key: string]: any;
|
if ("conditions" in c && c.conditions) {
|
||||||
|
array.push(...extractMediaQueries(c.conditions));
|
||||||
|
}
|
||||||
|
if (c.condition === "screen" && c.media_query) {
|
||||||
|
array.push(c.media_query);
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to setup media query listeners for conditional visibility
|
||||||
|
*/
|
||||||
|
export function setupMediaQueryListeners(
|
||||||
|
conditions: Condition[],
|
||||||
|
hass: HomeAssistant,
|
||||||
|
addListener: (unsub: () => void) => void,
|
||||||
|
onUpdate: (conditionsMet: boolean) => void
|
||||||
|
): void {
|
||||||
|
const mediaQueries = extractMediaQueries(conditions);
|
||||||
|
|
||||||
|
if (mediaQueries.length === 0) return;
|
||||||
|
|
||||||
|
// Optimization for single media query
|
||||||
|
const hasOnlyMediaQuery =
|
||||||
|
conditions.length === 1 &&
|
||||||
|
conditions[0].condition === "screen" &&
|
||||||
|
!!conditions[0].media_query;
|
||||||
|
|
||||||
|
mediaQueries.forEach((mediaQuery) => {
|
||||||
|
const unsub = listenMediaQuery(mediaQuery, (matches) => {
|
||||||
|
if (hasOnlyMediaQuery) {
|
||||||
|
onUpdate(matches);
|
||||||
|
} else {
|
||||||
|
const conditionsMet = checkConditionsMet(conditions, hass);
|
||||||
|
onUpdate(conditionsMet);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
addListener(unsub);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mixin to handle conditional listeners for visibility control
|
* Mixin to handle conditional listeners for visibility control
|
||||||
*
|
*
|
||||||
* Provides lifecycle management for listeners that control conditional
|
* Provides lifecycle management for listeners (media queries, time-based, state changes, etc.)
|
||||||
* visibility of components.
|
* that control conditional visibility of components.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* 1. Extend your component with ConditionalListenerMixin<YourConfigType>(ReactiveElement)
|
* 1. Extend your component with ConditionalListenerMixin(ReactiveElement)
|
||||||
* 2. Ensure component has config.visibility or _config.visibility property with conditions
|
* 2. Override setupConditionalListeners() to setup your listeners
|
||||||
* 3. Ensure component has _updateVisibility() or _updateElement() method
|
* 3. Use addConditionalListener() to register unsubscribe functions
|
||||||
* 4. Override setupConditionalListeners() if custom behavior needed (e.g., filter conditions)
|
* 4. Call clearConditionalListeners() and setupConditionalListeners() when config changes
|
||||||
*
|
*
|
||||||
* The mixin automatically:
|
* The mixin automatically:
|
||||||
* - Sets up listeners when component connects to DOM
|
* - Sets up listeners when component connects to DOM
|
||||||
* - Cleans up listeners when component disconnects from DOM
|
* - Cleans up listeners when component disconnects from DOM
|
||||||
* - Handles conditional visibility based on defined conditions
|
|
||||||
*/
|
*/
|
||||||
export const ConditionalListenerMixin = <
|
export const ConditionalListenerMixin = <
|
||||||
TConfig extends ConditionalConfig = ConditionalConfig,
|
T extends Constructor<ReactiveElement>,
|
||||||
>(
|
>(
|
||||||
superClass: Constructor<ReactiveElement>
|
superClass: T
|
||||||
) => {
|
) => {
|
||||||
abstract class ConditionalListenerClass extends superClass {
|
abstract class ConditionalListenerClass extends superClass {
|
||||||
private __listeners: (() => void)[] = [];
|
private __listeners: (() => void)[] = [];
|
||||||
|
|
||||||
protected _config?: TConfig;
|
|
||||||
|
|
||||||
public config?: TConfig;
|
|
||||||
|
|
||||||
public hass?: HomeAssistant;
|
|
||||||
|
|
||||||
protected _updateElement?(config: TConfig): void;
|
|
||||||
|
|
||||||
protected _updateVisibility?(conditionsMet?: boolean): void;
|
|
||||||
|
|
||||||
public connectedCallback() {
|
public connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.setupConditionalListeners();
|
this.setupConditionalListeners();
|
||||||
@@ -61,72 +87,17 @@ export const ConditionalListenerMixin = <
|
|||||||
this.clearConditionalListeners();
|
this.clearConditionalListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear conditional listeners
|
|
||||||
*
|
|
||||||
* This method is called when the component is disconnected from the DOM.
|
|
||||||
* It clears all the listeners that were set up by the setupConditionalListeners() method.
|
|
||||||
*/
|
|
||||||
protected clearConditionalListeners(): void {
|
protected clearConditionalListeners(): void {
|
||||||
this.__listeners.forEach((unsub) => unsub());
|
this.__listeners.forEach((unsub) => unsub());
|
||||||
this.__listeners = [];
|
this.__listeners = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a conditional listener to the list of listeners
|
|
||||||
*
|
|
||||||
* This method is called when a new listener is added.
|
|
||||||
* It adds the listener to the list of listeners.
|
|
||||||
*
|
|
||||||
* @param unsubscribe - The unsubscribe function to call when the listener is no longer needed
|
|
||||||
* @returns void
|
|
||||||
*/
|
|
||||||
protected addConditionalListener(unsubscribe: () => void): void {
|
protected addConditionalListener(unsubscribe: () => void): void {
|
||||||
this.__listeners.push(unsubscribe);
|
this.__listeners.push(unsubscribe);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected setupConditionalListeners(): void {
|
||||||
* Setup conditional listeners for visibility control
|
// Override in subclass
|
||||||
*
|
|
||||||
* Default implementation:
|
|
||||||
* - Checks config.visibility or _config.visibility for conditions (if not provided)
|
|
||||||
* - Sets up appropriate listeners based on condition types
|
|
||||||
* - Calls _updateVisibility() or _updateElement() when conditions change
|
|
||||||
*
|
|
||||||
* Override this method to customize behavior (e.g., filter conditions first)
|
|
||||||
* and call super.setupConditionalListeners(customConditions) to reuse the base implementation
|
|
||||||
*
|
|
||||||
* @param conditions - Optional conditions array. If not provided, will check config.visibility or _config.visibility
|
|
||||||
*/
|
|
||||||
protected setupConditionalListeners(conditions?: Condition[]): void {
|
|
||||||
const config = this.config || this._config;
|
|
||||||
const finalConditions = conditions || config?.visibility;
|
|
||||||
|
|
||||||
if (!finalConditions || !this.hass) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const onUpdate = (conditionsMet: boolean) => {
|
|
||||||
if (this._updateVisibility) {
|
|
||||||
this._updateVisibility(conditionsMet);
|
|
||||||
} else if (this._updateElement && config) {
|
|
||||||
this._updateElement(config);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
setupMediaQueryListeners(
|
|
||||||
finalConditions,
|
|
||||||
this.hass,
|
|
||||||
(unsub) => this.addConditionalListener(unsub),
|
|
||||||
onUpdate
|
|
||||||
);
|
|
||||||
|
|
||||||
setupTimeListeners(
|
|
||||||
finalConditions,
|
|
||||||
this.hass,
|
|
||||||
(unsub) => this.addConditionalListener(unsub),
|
|
||||||
onUpdate
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ConditionalListenerClass;
|
return ConditionalListenerClass;
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import type {
|
|||||||
EnergySource,
|
EnergySource,
|
||||||
FlowFromGridSourceEnergyPreference,
|
FlowFromGridSourceEnergyPreference,
|
||||||
FlowToGridSourceEnergyPreference,
|
FlowToGridSourceEnergyPreference,
|
||||||
GridPowerSourceEnergyPreference,
|
|
||||||
GridSourceTypeEnergyPreference,
|
GridSourceTypeEnergyPreference,
|
||||||
} from "../../../../data/energy";
|
} from "../../../../data/energy";
|
||||||
import {
|
import {
|
||||||
@@ -48,7 +47,6 @@ import { documentationUrl } from "../../../../util/documentation-url";
|
|||||||
import {
|
import {
|
||||||
showEnergySettingsGridFlowFromDialog,
|
showEnergySettingsGridFlowFromDialog,
|
||||||
showEnergySettingsGridFlowToDialog,
|
showEnergySettingsGridFlowToDialog,
|
||||||
showEnergySettingsGridPowerDialog,
|
|
||||||
} from "../dialogs/show-dialogs-energy";
|
} from "../dialogs/show-dialogs-energy";
|
||||||
import "./ha-energy-validation-result";
|
import "./ha-energy-validation-result";
|
||||||
import { energyCardStyles } from "./styles";
|
import { energyCardStyles } from "./styles";
|
||||||
@@ -228,58 +226,6 @@ export class EnergyGridSettings extends LitElement {
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>
|
|
||||||
${this.hass.localize("ui.panel.config.energy.grid.grid_power")}
|
|
||||||
</h3>
|
|
||||||
${gridSource.power?.map((power) => {
|
|
||||||
const entityState = this.hass.states[power.stat_rate];
|
|
||||||
return html`
|
|
||||||
<div class="row" .source=${power}>
|
|
||||||
${entityState?.attributes.icon
|
|
||||||
? html`<ha-icon
|
|
||||||
.icon=${entityState.attributes.icon}
|
|
||||||
></ha-icon>`
|
|
||||||
: html`<ha-svg-icon
|
|
||||||
.path=${mdiTransmissionTower}
|
|
||||||
></ha-svg-icon>`}
|
|
||||||
<span class="content"
|
|
||||||
>${getStatisticLabel(
|
|
||||||
this.hass,
|
|
||||||
power.stat_rate,
|
|
||||||
this.statsMetadata?.[power.stat_rate]
|
|
||||||
)}</span
|
|
||||||
>
|
|
||||||
<ha-icon-button
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.energy.grid.edit_power"
|
|
||||||
)}
|
|
||||||
@click=${this._editPowerSource}
|
|
||||||
.path=${mdiPencil}
|
|
||||||
></ha-icon-button>
|
|
||||||
<ha-icon-button
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.energy.grid.delete_power"
|
|
||||||
)}
|
|
||||||
@click=${this._deletePowerSource}
|
|
||||||
.path=${mdiDelete}
|
|
||||||
></ha-icon-button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
})}
|
|
||||||
<div class="row border-bottom">
|
|
||||||
<ha-svg-icon .path=${mdiTransmissionTower}></ha-svg-icon>
|
|
||||||
<ha-button
|
|
||||||
@click=${this._addPowerSource}
|
|
||||||
appearance="filled"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon
|
|
||||||
>${this.hass.localize(
|
|
||||||
"ui.panel.config.energy.grid.add_power"
|
|
||||||
)}</ha-button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3>
|
<h3>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.energy.grid.grid_carbon_footprint"
|
"ui.panel.config.energy.grid.grid_carbon_footprint"
|
||||||
@@ -553,97 +499,6 @@ export class EnergyGridSettings extends LitElement {
|
|||||||
await this._savePreferences(cleanedPreferences);
|
await this._savePreferences(cleanedPreferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _addPowerSource() {
|
|
||||||
const gridSource = this.preferences.energy_sources.find(
|
|
||||||
(src) => src.type === "grid"
|
|
||||||
) as GridSourceTypeEnergyPreference | undefined;
|
|
||||||
showEnergySettingsGridPowerDialog(this, {
|
|
||||||
grid_source: gridSource,
|
|
||||||
saveCallback: async (power) => {
|
|
||||||
let preferences: EnergyPreferences;
|
|
||||||
if (!gridSource) {
|
|
||||||
preferences = {
|
|
||||||
...this.preferences,
|
|
||||||
energy_sources: [
|
|
||||||
...this.preferences.energy_sources,
|
|
||||||
{
|
|
||||||
...emptyGridSourceEnergyPreference(),
|
|
||||||
power: [power],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
preferences = {
|
|
||||||
...this.preferences,
|
|
||||||
energy_sources: this.preferences.energy_sources.map((src) =>
|
|
||||||
src.type === "grid"
|
|
||||||
? { ...src, power: [...(gridSource.power || []), power] }
|
|
||||||
: src
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
await this._savePreferences(preferences);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _editPowerSource(ev) {
|
|
||||||
const origSource: GridPowerSourceEnergyPreference =
|
|
||||||
ev.currentTarget.closest(".row").source;
|
|
||||||
const gridSource = this.preferences.energy_sources.find(
|
|
||||||
(src) => src.type === "grid"
|
|
||||||
) as GridSourceTypeEnergyPreference | undefined;
|
|
||||||
showEnergySettingsGridPowerDialog(this, {
|
|
||||||
source: { ...origSource },
|
|
||||||
grid_source: gridSource,
|
|
||||||
saveCallback: async (source) => {
|
|
||||||
const power =
|
|
||||||
energySourcesByType(this.preferences).grid![0].power || [];
|
|
||||||
|
|
||||||
const preferences: EnergyPreferences = {
|
|
||||||
...this.preferences,
|
|
||||||
energy_sources: this.preferences.energy_sources.map((src) =>
|
|
||||||
src.type === "grid"
|
|
||||||
? {
|
|
||||||
...src,
|
|
||||||
power: power.map((p) => (p === origSource ? source : p)),
|
|
||||||
}
|
|
||||||
: src
|
|
||||||
),
|
|
||||||
};
|
|
||||||
await this._savePreferences(preferences);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _deletePowerSource(ev) {
|
|
||||||
const sourceToDelete: GridPowerSourceEnergyPreference =
|
|
||||||
ev.currentTarget.closest(".row").source;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(await showConfirmationDialog(this, {
|
|
||||||
title: this.hass.localize("ui.panel.config.energy.delete_source"),
|
|
||||||
}))
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const power =
|
|
||||||
energySourcesByType(this.preferences).grid![0].power?.filter(
|
|
||||||
(p) => p !== sourceToDelete
|
|
||||||
) || [];
|
|
||||||
|
|
||||||
const preferences: EnergyPreferences = {
|
|
||||||
...this.preferences,
|
|
||||||
energy_sources: this.preferences.energy_sources.map((source) =>
|
|
||||||
source.type === "grid" ? { ...source, power } : source
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanedPreferences = this._removeEmptySources(preferences);
|
|
||||||
await this._savePreferences(cleanedPreferences);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _removeEmptySources(preferences: EnergyPreferences) {
|
private _removeEmptySources(preferences: EnergyPreferences) {
|
||||||
// Check if grid sources became an empty type and remove if so
|
// Check if grid sources became an empty type and remove if so
|
||||||
preferences.energy_sources = preferences.energy_sources.reduce<
|
preferences.energy_sources = preferences.energy_sources.reduce<
|
||||||
@@ -652,8 +507,7 @@ export class EnergyGridSettings extends LitElement {
|
|||||||
if (
|
if (
|
||||||
source.type !== "grid" ||
|
source.type !== "grid" ||
|
||||||
source.flow_from.length > 0 ||
|
source.flow_from.length > 0 ||
|
||||||
source.flow_to.length > 0 ||
|
source.flow_to.length > 0
|
||||||
(source.power && source.power.length > 0)
|
|
||||||
) {
|
) {
|
||||||
acc.push(source);
|
acc.push(source);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import type { HomeAssistant } from "../../../../types";
|
|||||||
import type { EnergySettingsBatteryDialogParams } from "./show-dialogs-energy";
|
import type { EnergySettingsBatteryDialogParams } from "./show-dialogs-energy";
|
||||||
|
|
||||||
const energyUnitClasses = ["energy"];
|
const energyUnitClasses = ["energy"];
|
||||||
const powerUnitClasses = ["power"];
|
|
||||||
|
|
||||||
@customElement("dialog-energy-battery-settings")
|
@customElement("dialog-energy-battery-settings")
|
||||||
export class DialogEnergyBatterySettings
|
export class DialogEnergyBatterySettings
|
||||||
@@ -33,14 +32,10 @@ export class DialogEnergyBatterySettings
|
|||||||
|
|
||||||
@state() private _energy_units?: string[];
|
@state() private _energy_units?: string[];
|
||||||
|
|
||||||
@state() private _power_units?: string[];
|
|
||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
private _excludeList?: string[];
|
private _excludeList?: string[];
|
||||||
|
|
||||||
private _excludeListPower?: string[];
|
|
||||||
|
|
||||||
public async showDialog(
|
public async showDialog(
|
||||||
params: EnergySettingsBatteryDialogParams
|
params: EnergySettingsBatteryDialogParams
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -51,9 +46,6 @@ export class DialogEnergyBatterySettings
|
|||||||
this._energy_units = (
|
this._energy_units = (
|
||||||
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
|
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
|
||||||
).units;
|
).units;
|
||||||
this._power_units = (
|
|
||||||
await getSensorDeviceClassConvertibleUnits(this.hass, "power")
|
|
||||||
).units;
|
|
||||||
const allSources: string[] = [];
|
const allSources: string[] = [];
|
||||||
this._params.battery_sources.forEach((entry) => {
|
this._params.battery_sources.forEach((entry) => {
|
||||||
allSources.push(entry.stat_energy_from);
|
allSources.push(entry.stat_energy_from);
|
||||||
@@ -64,9 +56,6 @@ export class DialogEnergyBatterySettings
|
|||||||
id !== this._source?.stat_energy_from &&
|
id !== this._source?.stat_energy_from &&
|
||||||
id !== this._source?.stat_energy_to
|
id !== this._source?.stat_energy_to
|
||||||
);
|
);
|
||||||
this._excludeListPower = this._params.battery_sources
|
|
||||||
.map((entry) => entry.stat_rate)
|
|
||||||
.filter((id) => id && id !== this._source?.stat_rate) as string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog() {
|
public closeDialog() {
|
||||||
@@ -83,6 +72,8 @@ export class DialogEnergyBatterySettings
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pickableUnit = this._energy_units?.join(", ") || "";
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-dialog
|
<ha-dialog
|
||||||
open
|
open
|
||||||
@@ -94,6 +85,12 @@ export class DialogEnergyBatterySettings
|
|||||||
@closed=${this.closeDialog}
|
@closed=${this.closeDialog}
|
||||||
>
|
>
|
||||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||||
|
<div>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.battery.dialog.entity_para",
|
||||||
|
{ unit: pickableUnit }
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<ha-statistic-picker
|
<ha-statistic-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -108,10 +105,6 @@ export class DialogEnergyBatterySettings
|
|||||||
this._source.stat_energy_from,
|
this._source.stat_energy_from,
|
||||||
]}
|
]}
|
||||||
@value-changed=${this._statisticToChanged}
|
@value-changed=${this._statisticToChanged}
|
||||||
.helper=${this.hass.localize(
|
|
||||||
"ui.panel.config.energy.battery.dialog.energy_helper_into",
|
|
||||||
{ unit: this._energy_units?.join(", ") || "" }
|
|
||||||
)}
|
|
||||||
dialogInitialFocus
|
dialogInitialFocus
|
||||||
></ha-statistic-picker>
|
></ha-statistic-picker>
|
||||||
|
|
||||||
@@ -128,25 +121,6 @@ export class DialogEnergyBatterySettings
|
|||||||
this._source.stat_energy_to,
|
this._source.stat_energy_to,
|
||||||
]}
|
]}
|
||||||
@value-changed=${this._statisticFromChanged}
|
@value-changed=${this._statisticFromChanged}
|
||||||
.helper=${this.hass.localize(
|
|
||||||
"ui.panel.config.energy.battery.dialog.energy_helper_out",
|
|
||||||
{ unit: this._energy_units?.join(", ") || "" }
|
|
||||||
)}
|
|
||||||
></ha-statistic-picker>
|
|
||||||
|
|
||||||
<ha-statistic-picker
|
|
||||||
.hass=${this.hass}
|
|
||||||
.includeUnitClass=${powerUnitClasses}
|
|
||||||
.value=${this._source.stat_rate}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.energy.battery.dialog.power"
|
|
||||||
)}
|
|
||||||
.excludeStatistics=${this._excludeListPower}
|
|
||||||
@value-changed=${this._powerChanged}
|
|
||||||
.helper=${this.hass.localize(
|
|
||||||
"ui.panel.config.energy.battery.dialog.power_helper",
|
|
||||||
{ unit: this._power_units?.join(", ") || "" }
|
|
||||||
)}
|
|
||||||
></ha-statistic-picker>
|
></ha-statistic-picker>
|
||||||
|
|
||||||
<ha-button
|
<ha-button
|
||||||
@@ -176,10 +150,6 @@ export class DialogEnergyBatterySettings
|
|||||||
this._source = { ...this._source!, stat_energy_from: ev.detail.value };
|
this._source = { ...this._source!, stat_energy_from: ev.detail.value };
|
||||||
}
|
}
|
||||||
|
|
||||||
private _powerChanged(ev: CustomEvent<{ value: string }>) {
|
|
||||||
this._source = { ...this._source!, stat_rate: ev.detail.value };
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _save() {
|
private async _save() {
|
||||||
try {
|
try {
|
||||||
await this._params!.saveCallback(this._source!);
|
await this._params!.saveCallback(this._source!);
|
||||||
@@ -198,11 +168,7 @@ export class DialogEnergyBatterySettings
|
|||||||
--mdc-dialog-max-width: 430px;
|
--mdc-dialog-max-width: 430px;
|
||||||
}
|
}
|
||||||
ha-statistic-picker {
|
ha-statistic-picker {
|
||||||
display: block;
|
width: 100%;
|
||||||
margin-bottom: var(--ha-space-4);
|
|
||||||
}
|
|
||||||
ha-statistic-picker:last-of-type {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import type { HomeAssistant } from "../../../../types";
|
|||||||
import type { EnergySettingsDeviceDialogParams } from "./show-dialogs-energy";
|
import type { EnergySettingsDeviceDialogParams } from "./show-dialogs-energy";
|
||||||
|
|
||||||
const energyUnitClasses = ["energy"];
|
const energyUnitClasses = ["energy"];
|
||||||
const powerUnitClasses = ["power"];
|
|
||||||
|
|
||||||
@customElement("dialog-energy-device-settings")
|
@customElement("dialog-energy-device-settings")
|
||||||
export class DialogEnergyDeviceSettings
|
export class DialogEnergyDeviceSettings
|
||||||
@@ -36,14 +35,10 @@ export class DialogEnergyDeviceSettings
|
|||||||
|
|
||||||
@state() private _energy_units?: string[];
|
@state() private _energy_units?: string[];
|
||||||
|
|
||||||
@state() private _power_units?: string[];
|
|
||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
private _excludeList?: string[];
|
private _excludeList?: string[];
|
||||||
|
|
||||||
private _excludeListPower?: string[];
|
|
||||||
|
|
||||||
private _possibleParents: DeviceConsumptionEnergyPreference[] = [];
|
private _possibleParents: DeviceConsumptionEnergyPreference[] = [];
|
||||||
|
|
||||||
public async showDialog(
|
public async showDialog(
|
||||||
@@ -55,15 +50,9 @@ export class DialogEnergyDeviceSettings
|
|||||||
this._energy_units = (
|
this._energy_units = (
|
||||||
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
|
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
|
||||||
).units;
|
).units;
|
||||||
this._power_units = (
|
|
||||||
await getSensorDeviceClassConvertibleUnits(this.hass, "power")
|
|
||||||
).units;
|
|
||||||
this._excludeList = this._params.device_consumptions
|
this._excludeList = this._params.device_consumptions
|
||||||
.map((entry) => entry.stat_consumption)
|
.map((entry) => entry.stat_consumption)
|
||||||
.filter((id) => id !== this._device?.stat_consumption);
|
.filter((id) => id !== this._device?.stat_consumption);
|
||||||
this._excludeListPower = this._params.device_consumptions
|
|
||||||
.map((entry) => entry.stat_rate)
|
|
||||||
.filter((id) => id && id !== this._device?.stat_rate) as string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _computePossibleParents() {
|
private _computePossibleParents() {
|
||||||
@@ -104,6 +93,8 @@ export class DialogEnergyDeviceSettings
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pickableUnit = this._energy_units?.join(", ") || "";
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-dialog
|
<ha-dialog
|
||||||
open
|
open
|
||||||
@@ -117,6 +108,12 @@ export class DialogEnergyDeviceSettings
|
|||||||
@closed=${this.closeDialog}
|
@closed=${this.closeDialog}
|
||||||
>
|
>
|
||||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||||
|
<div>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.device_consumption.dialog.selected_stat_intro",
|
||||||
|
{ unit: pickableUnit }
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<ha-statistic-picker
|
<ha-statistic-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -128,28 +125,9 @@ export class DialogEnergyDeviceSettings
|
|||||||
)}
|
)}
|
||||||
.excludeStatistics=${this._excludeList}
|
.excludeStatistics=${this._excludeList}
|
||||||
@value-changed=${this._statisticChanged}
|
@value-changed=${this._statisticChanged}
|
||||||
.helper=${this.hass.localize(
|
|
||||||
"ui.panel.config.energy.device_consumption.dialog.selected_stat_intro",
|
|
||||||
{ unit: this._energy_units?.join(", ") || "" }
|
|
||||||
)}
|
|
||||||
dialogInitialFocus
|
dialogInitialFocus
|
||||||
></ha-statistic-picker>
|
></ha-statistic-picker>
|
||||||
|
|
||||||
<ha-statistic-picker
|
|
||||||
.hass=${this.hass}
|
|
||||||
.includeUnitClass=${powerUnitClasses}
|
|
||||||
.value=${this._device?.stat_rate}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.energy.device_consumption.dialog.device_consumption_power"
|
|
||||||
)}
|
|
||||||
.excludeStatistics=${this._excludeListPower}
|
|
||||||
@value-changed=${this._powerStatisticChanged}
|
|
||||||
.helper=${this.hass.localize(
|
|
||||||
"ui.panel.config.energy.device_consumption.dialog.selected_stat_intro",
|
|
||||||
{ unit: this._power_units?.join(", ") || "" }
|
|
||||||
)}
|
|
||||||
></ha-statistic-picker>
|
|
||||||
|
|
||||||
<ha-textfield
|
<ha-textfield
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.panel.config.energy.device_consumption.dialog.display_name"
|
"ui.panel.config.energy.device_consumption.dialog.display_name"
|
||||||
@@ -232,20 +210,6 @@ export class DialogEnergyDeviceSettings
|
|||||||
this._computePossibleParents();
|
this._computePossibleParents();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) {
|
|
||||||
if (!this._device) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newDevice = {
|
|
||||||
...this._device,
|
|
||||||
stat_rate: ev.detail.value,
|
|
||||||
} as DeviceConsumptionEnergyPreference;
|
|
||||||
if (!newDevice.stat_rate) {
|
|
||||||
delete newDevice.stat_rate;
|
|
||||||
}
|
|
||||||
this._device = newDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _nameChanged(ev) {
|
private _nameChanged(ev) {
|
||||||
const newDevice = {
|
const newDevice = {
|
||||||
...this._device!,
|
...this._device!,
|
||||||
@@ -281,19 +245,15 @@ export class DialogEnergyDeviceSettings
|
|||||||
return [
|
return [
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
css`
|
css`
|
||||||
ha-statistic-picker {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: var(--ha-space-2);
|
|
||||||
}
|
|
||||||
ha-statistic-picker {
|
ha-statistic-picker {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
ha-select {
|
ha-select {
|
||||||
margin-top: var(--ha-space-4);
|
margin-top: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
ha-textfield {
|
ha-textfield {
|
||||||
margin-top: var(--ha-space-4);
|
margin-top: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ export class DialogEnergyGridFlowSettings
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pickableUnit = this._energy_units?.join(", ") || "";
|
||||||
|
|
||||||
const unitPriceFixed = `${this.hass.config.currency}/kWh`;
|
const unitPriceFixed = `${this.hass.config.currency}/kWh`;
|
||||||
|
|
||||||
const externalSource =
|
const externalSource =
|
||||||
@@ -133,11 +135,19 @@ export class DialogEnergyGridFlowSettings
|
|||||||
@closed=${this.closeDialog}
|
@closed=${this.closeDialog}
|
||||||
>
|
>
|
||||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||||
|
<div>
|
||||||
<p>
|
<p>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.paragraph`
|
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.paragraph`
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
${this.hass.localize(
|
||||||
|
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.entity_para`,
|
||||||
|
{ unit: pickableUnit }
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ha-statistic-picker
|
<ha-statistic-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -153,10 +163,6 @@ export class DialogEnergyGridFlowSettings
|
|||||||
)}
|
)}
|
||||||
.excludeStatistics=${this._excludeList}
|
.excludeStatistics=${this._excludeList}
|
||||||
@value-changed=${this._statisticChanged}
|
@value-changed=${this._statisticChanged}
|
||||||
.helper=${this.hass.localize(
|
|
||||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.entity_para`,
|
|
||||||
{ unit: this._energy_units?.join(", ") || "" }
|
|
||||||
)}
|
|
||||||
dialogInitialFocus
|
dialogInitialFocus
|
||||||
></ha-statistic-picker>
|
></ha-statistic-picker>
|
||||||
|
|
||||||
@@ -355,10 +361,6 @@ export class DialogEnergyGridFlowSettings
|
|||||||
ha-dialog {
|
ha-dialog {
|
||||||
--mdc-dialog-max-width: 430px;
|
--mdc-dialog-max-width: 430px;
|
||||||
}
|
}
|
||||||
ha-statistic-picker {
|
|
||||||
display: block;
|
|
||||||
margin: var(--ha-space-4) 0;
|
|
||||||
}
|
|
||||||
ha-formfield {
|
ha-formfield {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,153 +0,0 @@
|
|||||||
import { mdiTransmissionTower } from "@mdi/js";
|
|
||||||
import type { CSSResultGroup } from "lit";
|
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
|
||||||
import "../../../../components/entity/ha-statistic-picker";
|
|
||||||
import "../../../../components/ha-dialog";
|
|
||||||
import "../../../../components/ha-button";
|
|
||||||
import type { GridPowerSourceEnergyPreference } from "../../../../data/energy";
|
|
||||||
import { energyStatisticHelpUrl } from "../../../../data/energy";
|
|
||||||
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
|
|
||||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
|
||||||
import { haStyleDialog } from "../../../../resources/styles";
|
|
||||||
import type { HomeAssistant } from "../../../../types";
|
|
||||||
import type { EnergySettingsGridPowerDialogParams } from "./show-dialogs-energy";
|
|
||||||
|
|
||||||
const powerUnitClasses = ["power"];
|
|
||||||
|
|
||||||
@customElement("dialog-energy-grid-power-settings")
|
|
||||||
export class DialogEnergyGridPowerSettings
|
|
||||||
extends LitElement
|
|
||||||
implements HassDialog<EnergySettingsGridPowerDialogParams>
|
|
||||||
{
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@state() private _params?: EnergySettingsGridPowerDialogParams;
|
|
||||||
|
|
||||||
@state() private _source?: GridPowerSourceEnergyPreference;
|
|
||||||
|
|
||||||
@state() private _power_units?: string[];
|
|
||||||
|
|
||||||
@state() private _error?: string;
|
|
||||||
|
|
||||||
private _excludeListPower?: string[];
|
|
||||||
|
|
||||||
public async showDialog(
|
|
||||||
params: EnergySettingsGridPowerDialogParams
|
|
||||||
): Promise<void> {
|
|
||||||
this._params = params;
|
|
||||||
this._source = params.source ? { ...params.source } : { stat_rate: "" };
|
|
||||||
|
|
||||||
const initialSourceIdPower = this._source.stat_rate;
|
|
||||||
|
|
||||||
this._power_units = (
|
|
||||||
await getSensorDeviceClassConvertibleUnits(this.hass, "power")
|
|
||||||
).units;
|
|
||||||
|
|
||||||
this._excludeListPower = [
|
|
||||||
...(this._params.grid_source?.power?.map((entry) => entry.stat_rate) ||
|
|
||||||
[]),
|
|
||||||
].filter((id) => id && id !== initialSourceIdPower) as string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
public closeDialog() {
|
|
||||||
this._params = undefined;
|
|
||||||
this._source = undefined;
|
|
||||||
this._error = undefined;
|
|
||||||
this._excludeListPower = undefined;
|
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
if (!this._params || !this._source) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<ha-dialog
|
|
||||||
open
|
|
||||||
.heading=${html`<ha-svg-icon
|
|
||||||
.path=${mdiTransmissionTower}
|
|
||||||
style="--mdc-icon-size: 32px;"
|
|
||||||
></ha-svg-icon
|
|
||||||
>${this.hass.localize(
|
|
||||||
"ui.panel.config.energy.grid.power_dialog.header"
|
|
||||||
)}`}
|
|
||||||
@closed=${this.closeDialog}
|
|
||||||
>
|
|
||||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
|
||||||
|
|
||||||
<ha-statistic-picker
|
|
||||||
.hass=${this.hass}
|
|
||||||
.helpMissingEntityUrl=${energyStatisticHelpUrl}
|
|
||||||
.includeUnitClass=${powerUnitClasses}
|
|
||||||
.value=${this._source.stat_rate}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.energy.grid.power_dialog.power_stat"
|
|
||||||
)}
|
|
||||||
.excludeStatistics=${this._excludeListPower}
|
|
||||||
@value-changed=${this._powerStatisticChanged}
|
|
||||||
.helper=${this.hass.localize(
|
|
||||||
"ui.panel.config.energy.grid.power_dialog.power_helper",
|
|
||||||
{ unit: this._power_units?.join(", ") || "" }
|
|
||||||
)}
|
|
||||||
dialogInitialFocus
|
|
||||||
></ha-statistic-picker>
|
|
||||||
|
|
||||||
<ha-button
|
|
||||||
appearance="plain"
|
|
||||||
@click=${this.closeDialog}
|
|
||||||
slot="primaryAction"
|
|
||||||
>
|
|
||||||
${this.hass.localize("ui.common.cancel")}
|
|
||||||
</ha-button>
|
|
||||||
<ha-button
|
|
||||||
@click=${this._save}
|
|
||||||
.disabled=${!this._source.stat_rate}
|
|
||||||
slot="primaryAction"
|
|
||||||
>
|
|
||||||
${this.hass.localize("ui.common.save")}
|
|
||||||
</ha-button>
|
|
||||||
</ha-dialog>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) {
|
|
||||||
this._source = {
|
|
||||||
...this._source!,
|
|
||||||
stat_rate: ev.detail.value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _save() {
|
|
||||||
try {
|
|
||||||
await this._params!.saveCallback(this._source!);
|
|
||||||
this.closeDialog();
|
|
||||||
} catch (err: any) {
|
|
||||||
this._error = err.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return [
|
|
||||||
haStyleDialog,
|
|
||||||
css`
|
|
||||||
ha-dialog {
|
|
||||||
--mdc-dialog-max-width: 430px;
|
|
||||||
}
|
|
||||||
ha-statistic-picker {
|
|
||||||
display: block;
|
|
||||||
margin: var(--ha-space-4) 0;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"dialog-energy-grid-power-settings": DialogEnergyGridPowerSettings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -28,7 +28,6 @@ import { brandsUrl } from "../../../../util/brands-url";
|
|||||||
import type { EnergySettingsSolarDialogParams } from "./show-dialogs-energy";
|
import type { EnergySettingsSolarDialogParams } from "./show-dialogs-energy";
|
||||||
|
|
||||||
const energyUnitClasses = ["energy"];
|
const energyUnitClasses = ["energy"];
|
||||||
const powerUnitClasses = ["power"];
|
|
||||||
|
|
||||||
@customElement("dialog-energy-solar-settings")
|
@customElement("dialog-energy-solar-settings")
|
||||||
export class DialogEnergySolarSettings
|
export class DialogEnergySolarSettings
|
||||||
@@ -47,14 +46,10 @@ export class DialogEnergySolarSettings
|
|||||||
|
|
||||||
@state() private _energy_units?: string[];
|
@state() private _energy_units?: string[];
|
||||||
|
|
||||||
@state() private _power_units?: string[];
|
|
||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
private _excludeList?: string[];
|
private _excludeList?: string[];
|
||||||
|
|
||||||
private _excludeListPower?: string[];
|
|
||||||
|
|
||||||
public async showDialog(
|
public async showDialog(
|
||||||
params: EnergySettingsSolarDialogParams
|
params: EnergySettingsSolarDialogParams
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -67,15 +62,9 @@ export class DialogEnergySolarSettings
|
|||||||
this._energy_units = (
|
this._energy_units = (
|
||||||
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
|
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
|
||||||
).units;
|
).units;
|
||||||
this._power_units = (
|
|
||||||
await getSensorDeviceClassConvertibleUnits(this.hass, "power")
|
|
||||||
).units;
|
|
||||||
this._excludeList = this._params.solar_sources
|
this._excludeList = this._params.solar_sources
|
||||||
.map((entry) => entry.stat_energy_from)
|
.map((entry) => entry.stat_energy_from)
|
||||||
.filter((id) => id !== this._source?.stat_energy_from);
|
.filter((id) => id !== this._source?.stat_energy_from);
|
||||||
this._excludeListPower = this._params.solar_sources
|
|
||||||
.map((entry) => entry.stat_rate)
|
|
||||||
.filter((id) => id && id !== this._source?.stat_rate) as string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog() {
|
public closeDialog() {
|
||||||
@@ -92,6 +81,8 @@ export class DialogEnergySolarSettings
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pickableUnit = this._energy_units?.join(", ") || "";
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-dialog
|
<ha-dialog
|
||||||
open
|
open
|
||||||
@@ -103,6 +94,12 @@ export class DialogEnergySolarSettings
|
|||||||
@closed=${this.closeDialog}
|
@closed=${this.closeDialog}
|
||||||
>
|
>
|
||||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||||
|
<div>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.solar.dialog.entity_para",
|
||||||
|
{ unit: pickableUnit }
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<ha-statistic-picker
|
<ha-statistic-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -114,28 +111,9 @@ export class DialogEnergySolarSettings
|
|||||||
)}
|
)}
|
||||||
.excludeStatistics=${this._excludeList}
|
.excludeStatistics=${this._excludeList}
|
||||||
@value-changed=${this._statisticChanged}
|
@value-changed=${this._statisticChanged}
|
||||||
.helper=${this.hass.localize(
|
|
||||||
"ui.panel.config.energy.solar.dialog.entity_para",
|
|
||||||
{ unit: this._energy_units?.join(", ") || "" }
|
|
||||||
)}
|
|
||||||
dialogInitialFocus
|
dialogInitialFocus
|
||||||
></ha-statistic-picker>
|
></ha-statistic-picker>
|
||||||
|
|
||||||
<ha-statistic-picker
|
|
||||||
.hass=${this.hass}
|
|
||||||
.includeUnitClass=${powerUnitClasses}
|
|
||||||
.value=${this._source.stat_rate}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.energy.solar.dialog.solar_production_power"
|
|
||||||
)}
|
|
||||||
.excludeStatistics=${this._excludeListPower}
|
|
||||||
@value-changed=${this._powerStatisticChanged}
|
|
||||||
.helper=${this.hass.localize(
|
|
||||||
"ui.panel.config.energy.solar.dialog.entity_para",
|
|
||||||
{ unit: this._power_units?.join(", ") || "" }
|
|
||||||
)}
|
|
||||||
></ha-statistic-picker>
|
|
||||||
|
|
||||||
<h3>
|
<h3>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.energy.solar.dialog.solar_production_forecast"
|
"ui.panel.config.energy.solar.dialog.solar_production_forecast"
|
||||||
@@ -289,10 +267,6 @@ export class DialogEnergySolarSettings
|
|||||||
this._source = { ...this._source!, stat_energy_from: ev.detail.value };
|
this._source = { ...this._source!, stat_energy_from: ev.detail.value };
|
||||||
}
|
}
|
||||||
|
|
||||||
private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) {
|
|
||||||
this._source = { ...this._source!, stat_rate: ev.detail.value };
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _save() {
|
private async _save() {
|
||||||
try {
|
try {
|
||||||
if (!this._forecast) {
|
if (!this._forecast) {
|
||||||
@@ -313,10 +287,6 @@ export class DialogEnergySolarSettings
|
|||||||
ha-dialog {
|
ha-dialog {
|
||||||
--mdc-dialog-max-width: 430px;
|
--mdc-dialog-max-width: 430px;
|
||||||
}
|
}
|
||||||
ha-statistic-picker {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: var(--ha-space-4);
|
|
||||||
}
|
|
||||||
img {
|
img {
|
||||||
height: 24px;
|
height: 24px;
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import type {
|
|||||||
FlowFromGridSourceEnergyPreference,
|
FlowFromGridSourceEnergyPreference,
|
||||||
FlowToGridSourceEnergyPreference,
|
FlowToGridSourceEnergyPreference,
|
||||||
GasSourceTypeEnergyPreference,
|
GasSourceTypeEnergyPreference,
|
||||||
GridPowerSourceEnergyPreference,
|
|
||||||
GridSourceTypeEnergyPreference,
|
GridSourceTypeEnergyPreference,
|
||||||
SolarSourceTypeEnergyPreference,
|
SolarSourceTypeEnergyPreference,
|
||||||
WaterSourceTypeEnergyPreference,
|
WaterSourceTypeEnergyPreference,
|
||||||
@@ -42,12 +41,6 @@ export interface EnergySettingsGridFlowToDialogParams {
|
|||||||
saveCallback: (source: FlowToGridSourceEnergyPreference) => Promise<void>;
|
saveCallback: (source: FlowToGridSourceEnergyPreference) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EnergySettingsGridPowerDialogParams {
|
|
||||||
source?: GridPowerSourceEnergyPreference;
|
|
||||||
grid_source?: GridSourceTypeEnergyPreference;
|
|
||||||
saveCallback: (source: GridPowerSourceEnergyPreference) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EnergySettingsSolarDialogParams {
|
export interface EnergySettingsSolarDialogParams {
|
||||||
info: EnergyInfo;
|
info: EnergyInfo;
|
||||||
source?: SolarSourceTypeEnergyPreference;
|
source?: SolarSourceTypeEnergyPreference;
|
||||||
@@ -159,14 +152,3 @@ export const showEnergySettingsGridFlowToDialog = (
|
|||||||
dialogParams: { ...dialogParams, direction: "to" },
|
dialogParams: { ...dialogParams, direction: "to" },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const showEnergySettingsGridPowerDialog = (
|
|
||||||
element: HTMLElement,
|
|
||||||
dialogParams: EnergySettingsGridPowerDialogParams
|
|
||||||
): void => {
|
|
||||||
fireEvent(element, "show-dialog", {
|
|
||||||
dialogTag: "dialog-energy-grid-power-settings",
|
|
||||||
dialogImport: () => import("./dialog-energy-grid-power-settings"),
|
|
||||||
dialogParams: dialogParams,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -360,20 +360,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.hass.panels.home) {
|
|
||||||
result.push({
|
|
||||||
icon: this.hass.panels.home.icon || "mdi:home",
|
|
||||||
title: this.hass.localize("panel.home"),
|
|
||||||
show_in_sidebar: true,
|
|
||||||
mode: "storage",
|
|
||||||
url_path: "home",
|
|
||||||
filename: "",
|
|
||||||
default: false,
|
|
||||||
require_admin: false,
|
|
||||||
type: this._localizeType("built_in"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(
|
result.push(
|
||||||
...dashboards
|
...dashboards
|
||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
@@ -484,18 +470,13 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _canDelete(urlPath: string) {
|
private _canDelete(urlPath: string) {
|
||||||
return ![
|
return !["lovelace", "energy", "light", "security", "climate"].includes(
|
||||||
"lovelace",
|
urlPath
|
||||||
"energy",
|
);
|
||||||
"light",
|
|
||||||
"security",
|
|
||||||
"climate",
|
|
||||||
"home",
|
|
||||||
].includes(urlPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _canEdit(urlPath: string) {
|
private _canEdit(urlPath: string) {
|
||||||
return !["light", "security", "climate", "home"].includes(urlPath);
|
return !["light", "security", "climate"].includes(urlPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleDelete = async (item: DataTableItem) => {
|
private _handleDelete = async (item: DataTableItem) => {
|
||||||
|
|||||||
@@ -65,15 +65,13 @@ export default class HaScriptFieldRow extends LitElement {
|
|||||||
private _selectorRowElement?: HaAutomationRow;
|
private _selectorRowElement?: HaAutomationRow;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const hasSelector =
|
|
||||||
this.field.selector && typeof this.field.selector === "object";
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card outlined>
|
<ha-card outlined>
|
||||||
<ha-automation-row
|
<ha-automation-row
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
@click=${this._toggleSidebar}
|
@click=${this._toggleSidebar}
|
||||||
.selected=${this._selected}
|
.selected=${this._selected}
|
||||||
.leftChevron=${hasSelector}
|
left-chevron
|
||||||
@toggle-collapsed=${this._toggleCollapse}
|
@toggle-collapsed=${this._toggleCollapse}
|
||||||
.collapsed=${this._collapsed}
|
.collapsed=${this._collapsed}
|
||||||
.highlight=${this.highlight}
|
.highlight=${this.highlight}
|
||||||
@@ -142,8 +140,6 @@ export default class HaScriptFieldRow extends LitElement {
|
|||||||
<slot name="icons" slot="icons"></slot>
|
<slot name="icons" slot="icons"></slot>
|
||||||
</ha-automation-row>
|
</ha-automation-row>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
${hasSelector
|
|
||||||
? html`
|
|
||||||
<div
|
<div
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
"selector-row": true,
|
"selector-row": true,
|
||||||
@@ -207,10 +203,7 @@ export default class HaScriptFieldRow extends LitElement {
|
|||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
class="warning"
|
class="warning"
|
||||||
>
|
>
|
||||||
<ha-svg-icon
|
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
|
||||||
slot="start"
|
|
||||||
.path=${mdiDelete}
|
|
||||||
></ha-svg-icon>
|
|
||||||
<div class="overflow-label">
|
<div class="overflow-label">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.automation.editor.actions.delete"
|
"ui.panel.config.automation.editor.actions.delete"
|
||||||
@@ -258,8 +251,6 @@ export default class HaScriptFieldRow extends LitElement {
|
|||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
|
||||||
import { LitElement, css, html, nothing } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import { debounce } from "../../common/util/debounce";
|
|
||||||
import { deepEqual } from "../../common/util/deep-equal";
|
|
||||||
import type { LovelaceDashboardStrategyConfig } from "../../data/lovelace/config/types";
|
|
||||||
import type { HomeAssistant, PanelInfo, Route } from "../../types";
|
|
||||||
import "../lovelace/hui-root";
|
|
||||||
import { generateLovelaceDashboardStrategy } from "../lovelace/strategies/get-strategy";
|
|
||||||
import type { Lovelace } from "../lovelace/types";
|
|
||||||
import { showAlertDialog } from "../lovelace/custom-card-helpers";
|
|
||||||
|
|
||||||
const HOME_LOVELACE_CONFIG: LovelaceDashboardStrategyConfig = {
|
|
||||||
strategy: {
|
|
||||||
type: "home",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
@customElement("ha-panel-home")
|
|
||||||
class PanelHome extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public route?: Route;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public panel?: PanelInfo;
|
|
||||||
|
|
||||||
@state() private _lovelace?: Lovelace;
|
|
||||||
|
|
||||||
public willUpdate(changedProps: PropertyValues) {
|
|
||||||
super.willUpdate(changedProps);
|
|
||||||
// Initial setup
|
|
||||||
if (!this.hasUpdated) {
|
|
||||||
this.hass.loadFragmentTranslation("lovelace");
|
|
||||||
this._setLovelace();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!changedProps.has("hass")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldHass = changedProps.get("hass") as this["hass"];
|
|
||||||
if (oldHass && oldHass.localize !== this.hass.localize) {
|
|
||||||
this._setLovelace();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldHass && this.hass) {
|
|
||||||
// If the entity registry changed, ask the user if they want to refresh the config
|
|
||||||
if (
|
|
||||||
oldHass.entities !== this.hass.entities ||
|
|
||||||
oldHass.devices !== this.hass.devices ||
|
|
||||||
oldHass.areas !== this.hass.areas ||
|
|
||||||
oldHass.floors !== this.hass.floors
|
|
||||||
) {
|
|
||||||
if (this.hass.config.state === "RUNNING") {
|
|
||||||
this._debounceRegistriesChanged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If ha started, refresh the config
|
|
||||||
if (
|
|
||||||
this.hass.config.state === "RUNNING" &&
|
|
||||||
oldHass.config.state !== "RUNNING"
|
|
||||||
) {
|
|
||||||
this._setLovelace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _debounceRegistriesChanged = debounce(
|
|
||||||
() => this._registriesChanged(),
|
|
||||||
200
|
|
||||||
);
|
|
||||||
|
|
||||||
private _registriesChanged = async () => {
|
|
||||||
this._setLovelace();
|
|
||||||
};
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
if (!this._lovelace) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<hui-root
|
|
||||||
.hass=${this.hass}
|
|
||||||
.narrow=${this.narrow}
|
|
||||||
.lovelace=${this._lovelace}
|
|
||||||
.route=${this.route}
|
|
||||||
.panel=${this.panel}
|
|
||||||
></hui-root>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _setLovelace() {
|
|
||||||
const config = await generateLovelaceDashboardStrategy(
|
|
||||||
HOME_LOVELACE_CONFIG,
|
|
||||||
this.hass
|
|
||||||
);
|
|
||||||
|
|
||||||
if (deepEqual(config, this._lovelace?.config)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._lovelace = {
|
|
||||||
config: config,
|
|
||||||
rawConfig: config,
|
|
||||||
editMode: false,
|
|
||||||
urlPath: "home",
|
|
||||||
mode: "generated",
|
|
||||||
locale: this.hass.locale,
|
|
||||||
enableFullEditMode: () => undefined,
|
|
||||||
saveConfig: async () => undefined,
|
|
||||||
deleteConfig: async () => undefined,
|
|
||||||
setEditMode: this._setEditMode,
|
|
||||||
showToast: () => undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private _setEditMode = () => {
|
|
||||||
// For now, we just show an alert that edit mode is not supported.
|
|
||||||
// This will be expanded in the future.
|
|
||||||
showAlertDialog(this, {
|
|
||||||
title: "Edit mode not available",
|
|
||||||
text: "The Home panel does not support edit mode.",
|
|
||||||
confirmText: this.hass.localize("ui.common.ok"),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
static readonly styles: CSSResultGroup = css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-panel-home": PanelHome;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,10 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
|||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
|
import {
|
||||||
|
ConditionalListenerMixin,
|
||||||
|
setupMediaQueryListeners,
|
||||||
|
} from "../../../mixins/conditional-listener-mixin";
|
||||||
import { checkConditionsMet } from "../common/validate-condition";
|
import { checkConditionsMet } from "../common/validate-condition";
|
||||||
import { createBadgeElement } from "../create-element/create-badge-element";
|
import { createBadgeElement } from "../create-element/create-badge-element";
|
||||||
import { createErrorBadgeConfig } from "../create-element/create-element-base";
|
import { createErrorBadgeConfig } from "../create-element/create-element-base";
|
||||||
@@ -19,9 +22,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@customElement("hui-badge")
|
@customElement("hui-badge")
|
||||||
export class HuiBadge extends ConditionalListenerMixin<LovelaceBadgeConfig>(
|
export class HuiBadge extends ConditionalListenerMixin(ReactiveElement) {
|
||||||
ReactiveElement
|
|
||||||
) {
|
|
||||||
@property({ type: Boolean }) public preview = false;
|
@property({ type: Boolean }) public preview = false;
|
||||||
|
|
||||||
@property({ attribute: false }) public config?: LovelaceBadgeConfig;
|
@property({ attribute: false }) public config?: LovelaceBadgeConfig;
|
||||||
@@ -52,7 +53,7 @@ export class HuiBadge extends ConditionalListenerMixin<LovelaceBadgeConfig>(
|
|||||||
this._updateVisibility();
|
this._updateVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _updateElement(config: LovelaceBadgeConfig) {
|
private _updateElement(config: LovelaceBadgeConfig) {
|
||||||
if (!this._element) {
|
if (!this._element) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -132,7 +133,22 @@ export class HuiBadge extends ConditionalListenerMixin<LovelaceBadgeConfig>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _updateVisibility(conditionsMet?: boolean) {
|
protected setupConditionalListeners() {
|
||||||
|
if (!this.config?.visibility || !this.hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupMediaQueryListeners(
|
||||||
|
this.config.visibility,
|
||||||
|
this.hass,
|
||||||
|
(unsub) => this.addConditionalListener(unsub),
|
||||||
|
(conditionsMet) => {
|
||||||
|
this._updateVisibility(conditionsMet);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateVisibility(ignoreConditions?: boolean) {
|
||||||
if (!this._element || !this.hass) {
|
if (!this._element || !this.hass) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -153,9 +169,9 @@ export class HuiBadge extends ConditionalListenerMixin<LovelaceBadgeConfig>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const visible =
|
const visible =
|
||||||
conditionsMet ??
|
ignoreConditions ||
|
||||||
(!this.config?.visibility ||
|
!this.config?.visibility ||
|
||||||
checkConditionsMet(this.config.visibility, this.hass));
|
checkConditionsMet(this.config.visibility, this.hass);
|
||||||
this._setElementVisibility(visible);
|
this._setElementVisibility(visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
|||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
|
import {
|
||||||
|
ConditionalListenerMixin,
|
||||||
|
setupMediaQueryListeners,
|
||||||
|
} from "../../../mixins/conditional-listener-mixin";
|
||||||
import { migrateLayoutToGridOptions } from "../common/compute-card-grid-size";
|
import { migrateLayoutToGridOptions } from "../common/compute-card-grid-size";
|
||||||
import { computeCardSize } from "../common/compute-card-size";
|
import { computeCardSize } from "../common/compute-card-size";
|
||||||
import { checkConditionsMet } from "../common/validate-condition";
|
import { checkConditionsMet } from "../common/validate-condition";
|
||||||
@@ -21,9 +24,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@customElement("hui-card")
|
@customElement("hui-card")
|
||||||
export class HuiCard extends ConditionalListenerMixin<LovelaceCardConfig>(
|
export class HuiCard extends ConditionalListenerMixin(ReactiveElement) {
|
||||||
ReactiveElement
|
|
||||||
) {
|
|
||||||
@property({ type: Boolean }) public preview = false;
|
@property({ type: Boolean }) public preview = false;
|
||||||
|
|
||||||
@property({ attribute: false }) public config?: LovelaceCardConfig;
|
@property({ attribute: false }) public config?: LovelaceCardConfig;
|
||||||
@@ -120,7 +121,7 @@ export class HuiCard extends ConditionalListenerMixin<LovelaceCardConfig>(
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _updateElement(config: LovelaceCardConfig) {
|
private _updateElement(config: LovelaceCardConfig) {
|
||||||
if (!this._element) {
|
if (!this._element) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -246,7 +247,22 @@ export class HuiCard extends ConditionalListenerMixin<LovelaceCardConfig>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _updateVisibility(conditionsMet?: boolean) {
|
protected setupConditionalListeners() {
|
||||||
|
if (!this.config?.visibility || !this.hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupMediaQueryListeners(
|
||||||
|
this.config.visibility,
|
||||||
|
this.hass,
|
||||||
|
(unsub) => this.addConditionalListener(unsub),
|
||||||
|
(conditionsMet) => {
|
||||||
|
this._updateVisibility(conditionsMet);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateVisibility(ignoreConditions?: boolean) {
|
||||||
if (!this._element || !this.hass) {
|
if (!this._element || !this.hass) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -267,9 +283,9 @@ export class HuiCard extends ConditionalListenerMixin<LovelaceCardConfig>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const visible =
|
const visible =
|
||||||
conditionsMet ??
|
ignoreConditions ||
|
||||||
(!this.config?.visibility ||
|
!this.config?.visibility ||
|
||||||
checkConditionsMet(this.config.visibility, this.hass));
|
checkConditionsMet(this.config.visibility, this.hass);
|
||||||
this._setElementVisibility(visible);
|
this._setElementVisibility(visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
mdiAccount,
|
mdiAccount,
|
||||||
mdiAmpersand,
|
mdiAmpersand,
|
||||||
mdiCalendarClock,
|
|
||||||
mdiGateOr,
|
mdiGateOr,
|
||||||
mdiMapMarker,
|
mdiMapMarker,
|
||||||
mdiNotEqualVariant,
|
mdiNotEqualVariant,
|
||||||
@@ -16,7 +15,6 @@ export const ICON_CONDITION: Record<Condition["condition"], string> = {
|
|||||||
numeric_state: mdiNumeric,
|
numeric_state: mdiNumeric,
|
||||||
state: mdiStateMachine,
|
state: mdiStateMachine,
|
||||||
screen: mdiResponsive,
|
screen: mdiResponsive,
|
||||||
time: mdiCalendarClock,
|
|
||||||
user: mdiAccount,
|
user: mdiAccount,
|
||||||
and: mdiAmpersand,
|
and: mdiAmpersand,
|
||||||
not: mdiNotEqualVariant,
|
not: mdiNotEqualVariant,
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
import type { HomeAssistant } from "../../../types";
|
import { ensureArray } from "../../../common/array/ensure-array";
|
||||||
|
|
||||||
|
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||||
import { UNKNOWN } from "../../../data/entity";
|
import { UNKNOWN } from "../../../data/entity";
|
||||||
import { getUserPerson } from "../../../data/person";
|
import { getUserPerson } from "../../../data/person";
|
||||||
import { ensureArray } from "../../../common/array/ensure-array";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import {
|
|
||||||
checkTimeInRange,
|
|
||||||
isValidTimeString,
|
|
||||||
} from "../../../common/datetime/check_time";
|
|
||||||
import {
|
|
||||||
WEEKDAYS_SHORT,
|
|
||||||
type WeekdayShort,
|
|
||||||
} from "../../../common/datetime/weekday";
|
|
||||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
|
||||||
|
|
||||||
export type Condition =
|
export type Condition =
|
||||||
| LocationCondition
|
| LocationCondition
|
||||||
| NumericStateCondition
|
| NumericStateCondition
|
||||||
| StateCondition
|
| StateCondition
|
||||||
| ScreenCondition
|
| ScreenCondition
|
||||||
| TimeCondition
|
|
||||||
| UserCondition
|
| UserCondition
|
||||||
| OrCondition
|
| OrCondition
|
||||||
| AndCondition
|
| AndCondition
|
||||||
@@ -58,13 +50,6 @@ export interface ScreenCondition extends BaseCondition {
|
|||||||
media_query?: string;
|
media_query?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TimeCondition extends BaseCondition {
|
|
||||||
condition: "time";
|
|
||||||
after?: string;
|
|
||||||
before?: string;
|
|
||||||
weekdays?: WeekdayShort[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserCondition extends BaseCondition {
|
export interface UserCondition extends BaseCondition {
|
||||||
condition: "user";
|
condition: "user";
|
||||||
users?: string[];
|
users?: string[];
|
||||||
@@ -165,13 +150,6 @@ function checkScreenCondition(condition: ScreenCondition, _: HomeAssistant) {
|
|||||||
: false;
|
: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkTimeCondition(
|
|
||||||
condition: Omit<TimeCondition, "condition">,
|
|
||||||
hass: HomeAssistant
|
|
||||||
) {
|
|
||||||
return checkTimeInRange(hass, condition);
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkLocationCondition(
|
function checkLocationCondition(
|
||||||
condition: LocationCondition,
|
condition: LocationCondition,
|
||||||
hass: HomeAssistant
|
hass: HomeAssistant
|
||||||
@@ -217,8 +195,6 @@ export function checkConditionsMet(
|
|||||||
return conditions.every((c) => {
|
return conditions.every((c) => {
|
||||||
if ("condition" in c) {
|
if ("condition" in c) {
|
||||||
switch (c.condition) {
|
switch (c.condition) {
|
||||||
case "time":
|
|
||||||
return checkTimeCondition(c, hass);
|
|
||||||
case "screen":
|
case "screen":
|
||||||
return checkScreenCondition(c, hass);
|
return checkScreenCondition(c, hass);
|
||||||
case "user":
|
case "user":
|
||||||
@@ -295,35 +271,6 @@ function validateScreenCondition(condition: ScreenCondition) {
|
|||||||
return condition.media_query != null;
|
return condition.media_query != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateTimeCondition(condition: TimeCondition) {
|
|
||||||
// Check if time strings are present and non-empty
|
|
||||||
const hasAfter = condition.after != null && condition.after !== "";
|
|
||||||
const hasBefore = condition.before != null && condition.before !== "";
|
|
||||||
const hasTime = hasAfter || hasBefore;
|
|
||||||
|
|
||||||
const hasWeekdays =
|
|
||||||
condition.weekdays != null && condition.weekdays.length > 0;
|
|
||||||
const weekdaysValid =
|
|
||||||
!hasWeekdays ||
|
|
||||||
condition.weekdays!.every((w: WeekdayShort) => WEEKDAYS_SHORT.includes(w));
|
|
||||||
|
|
||||||
// Validate time string formats if present
|
|
||||||
const timeStringsValid =
|
|
||||||
(!hasAfter || isValidTimeString(condition.after!)) &&
|
|
||||||
(!hasBefore || isValidTimeString(condition.before!));
|
|
||||||
|
|
||||||
// Prevent after and before being identical (creates zero-length interval)
|
|
||||||
const timeRangeValid =
|
|
||||||
!hasAfter || !hasBefore || condition.after !== condition.before;
|
|
||||||
|
|
||||||
return (
|
|
||||||
(hasTime || hasWeekdays) &&
|
|
||||||
weekdaysValid &&
|
|
||||||
timeStringsValid &&
|
|
||||||
timeRangeValid
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateUserCondition(condition: UserCondition) {
|
function validateUserCondition(condition: UserCondition) {
|
||||||
return condition.users != null;
|
return condition.users != null;
|
||||||
}
|
}
|
||||||
@@ -363,8 +310,6 @@ export function validateConditionalConfig(
|
|||||||
switch (c.condition) {
|
switch (c.condition) {
|
||||||
case "screen":
|
case "screen":
|
||||||
return validateScreenCondition(c);
|
return validateScreenCondition(c);
|
||||||
case "time":
|
|
||||||
return validateTimeCondition(c);
|
|
||||||
case "user":
|
case "user":
|
||||||
return validateUserCondition(c);
|
return validateUserCondition(c);
|
||||||
case "location":
|
case "location":
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import type { PropertyValues } from "lit";
|
|||||||
import { ReactiveElement } from "lit";
|
import { ReactiveElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
|
import {
|
||||||
|
ConditionalListenerMixin,
|
||||||
|
setupMediaQueryListeners,
|
||||||
|
} from "../../../mixins/conditional-listener-mixin";
|
||||||
import type { HuiCard } from "../cards/hui-card";
|
import type { HuiCard } from "../cards/hui-card";
|
||||||
import type { ConditionalCardConfig } from "../cards/types";
|
import type { ConditionalCardConfig } from "../cards/types";
|
||||||
import type { Condition } from "../common/validate-condition";
|
import type { Condition } from "../common/validate-condition";
|
||||||
@@ -19,9 +22,9 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@customElement("hui-conditional-base")
|
@customElement("hui-conditional-base")
|
||||||
export class HuiConditionalBase extends ConditionalListenerMixin<
|
export class HuiConditionalBase extends ConditionalListenerMixin(
|
||||||
ConditionalCardConfig | ConditionalRowConfig
|
ReactiveElement
|
||||||
>(ReactiveElement) {
|
) {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property({ type: Boolean }) public preview = false;
|
@property({ type: Boolean }) public preview = false;
|
||||||
@@ -70,13 +73,18 @@ export class HuiConditionalBase extends ConditionalListenerMixin<
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter to supported conditions (those with 'condition' property)
|
|
||||||
const supportedConditions = this._config.conditions.filter(
|
const supportedConditions = this._config.conditions.filter(
|
||||||
(c) => "condition" in c
|
(c) => "condition" in c
|
||||||
) as Condition[];
|
) as Condition[];
|
||||||
|
|
||||||
// Pass filtered conditions to parent implementation
|
setupMediaQueryListeners(
|
||||||
super.setupConditionalListeners(supportedConditions);
|
supportedConditions,
|
||||||
|
this.hass,
|
||||||
|
(unsub) => this.addConditionalListener(unsub),
|
||||||
|
(conditionsMet) => {
|
||||||
|
this.setVisibility(conditionsMet);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected update(changed: PropertyValues): void {
|
protected update(changed: PropertyValues): void {
|
||||||
@@ -94,15 +102,17 @@ export class HuiConditionalBase extends ConditionalListenerMixin<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _updateVisibility(conditionsMet?: boolean) {
|
private _updateVisibility() {
|
||||||
if (!this._element || !this.hass || !this._config) {
|
if (!this._element || !this.hass || !this._config) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._element.preview = this.preview;
|
this._element.preview = this.preview;
|
||||||
|
|
||||||
const conditionMet =
|
const conditionMet = checkConditionsMet(
|
||||||
conditionsMet ?? checkConditionsMet(this._config.conditions, this.hass);
|
this._config!.conditions,
|
||||||
|
this.hass!
|
||||||
|
);
|
||||||
|
|
||||||
this.setVisibility(conditionMet);
|
this.setVisibility(conditionMet);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import "./types/ha-card-condition-numeric_state";
|
|||||||
import "./types/ha-card-condition-or";
|
import "./types/ha-card-condition-or";
|
||||||
import "./types/ha-card-condition-screen";
|
import "./types/ha-card-condition-screen";
|
||||||
import "./types/ha-card-condition-state";
|
import "./types/ha-card-condition-state";
|
||||||
import "./types/ha-card-condition-time";
|
|
||||||
import "./types/ha-card-condition-user";
|
import "./types/ha-card-condition-user";
|
||||||
import { storage } from "../../../../common/decorators/storage";
|
import { storage } from "../../../../common/decorators/storage";
|
||||||
|
|
||||||
@@ -34,7 +33,6 @@ const UI_CONDITION = [
|
|||||||
"numeric_state",
|
"numeric_state",
|
||||||
"state",
|
"state",
|
||||||
"screen",
|
"screen",
|
||||||
"time",
|
|
||||||
"user",
|
"user",
|
||||||
"and",
|
"and",
|
||||||
"not",
|
"not",
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
import { html, LitElement } from "lit";
|
|
||||||
import { customElement, property } from "lit/decorators";
|
|
||||||
import {
|
|
||||||
literal,
|
|
||||||
array,
|
|
||||||
object,
|
|
||||||
optional,
|
|
||||||
string,
|
|
||||||
assert,
|
|
||||||
enums,
|
|
||||||
} from "superstruct";
|
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import type { HomeAssistant } from "../../../../../types";
|
|
||||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
|
||||||
import {
|
|
||||||
WEEKDAY_SHORT_TO_LONG,
|
|
||||||
WEEKDAYS_SHORT,
|
|
||||||
} from "../../../../../common/datetime/weekday";
|
|
||||||
import type { TimeCondition } from "../../../common/validate-condition";
|
|
||||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
|
||||||
import type {
|
|
||||||
HaFormSchema,
|
|
||||||
SchemaUnion,
|
|
||||||
} from "../../../../../components/ha-form/types";
|
|
||||||
import "../../../../../components/ha-form/ha-form";
|
|
||||||
|
|
||||||
const timeConditionStruct = object({
|
|
||||||
condition: literal("time"),
|
|
||||||
after: optional(string()),
|
|
||||||
before: optional(string()),
|
|
||||||
weekdays: optional(array(enums(WEEKDAYS_SHORT))),
|
|
||||||
});
|
|
||||||
|
|
||||||
@customElement("ha-card-condition-time")
|
|
||||||
export class HaCardConditionTime extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public condition!: TimeCondition;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
|
||||||
|
|
||||||
public static get defaultConfig(): TimeCondition {
|
|
||||||
return { condition: "time", after: "08:00", before: "17:00" };
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static validateUIConfig(condition: TimeCondition) {
|
|
||||||
return assert(condition, timeConditionStruct);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _schema = memoizeOne(
|
|
||||||
(localize: LocalizeFunc) =>
|
|
||||||
[
|
|
||||||
{ name: "after", selector: { time: { no_second: true } } },
|
|
||||||
{ name: "before", selector: { time: { no_second: true } } },
|
|
||||||
{
|
|
||||||
name: "weekdays",
|
|
||||||
selector: {
|
|
||||||
select: {
|
|
||||||
mode: "list",
|
|
||||||
multiple: true,
|
|
||||||
options: WEEKDAYS_SHORT.map((day) => ({
|
|
||||||
value: day,
|
|
||||||
label: localize(`ui.weekdays.${WEEKDAY_SHORT_TO_LONG[day]}`),
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as const satisfies HaFormSchema[]
|
|
||||||
);
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
return html`
|
|
||||||
<ha-form
|
|
||||||
.hass=${this.hass}
|
|
||||||
.data=${this.condition}
|
|
||||||
.computeLabel=${this._computeLabelCallback}
|
|
||||||
.schema=${this._schema(this.hass.localize)}
|
|
||||||
.disabled=${this.disabled}
|
|
||||||
@value-changed=${this._valueChanged}
|
|
||||||
></ha-form>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _valueChanged(ev: CustomEvent) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
const data = ev.detail.value as TimeCondition;
|
|
||||||
fireEvent(this, "value-changed", { value: data });
|
|
||||||
}
|
|
||||||
|
|
||||||
private _computeLabelCallback = (
|
|
||||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
|
||||||
): string =>
|
|
||||||
this.hass.localize(
|
|
||||||
`ui.panel.lovelace.editor.condition-editor.condition.time.${schema.name}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-card-condition-time": HaCardConditionTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,10 @@ import { customElement, property } from "lit/decorators";
|
|||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
|
import {
|
||||||
|
ConditionalListenerMixin,
|
||||||
|
setupMediaQueryListeners,
|
||||||
|
} from "../../../mixins/conditional-listener-mixin";
|
||||||
import { checkConditionsMet } from "../common/validate-condition";
|
import { checkConditionsMet } from "../common/validate-condition";
|
||||||
import { createHeadingBadgeElement } from "../create-element/create-heading-badge-element";
|
import { createHeadingBadgeElement } from "../create-element/create-heading-badge-element";
|
||||||
import type { LovelaceHeadingBadge } from "../types";
|
import type { LovelaceHeadingBadge } from "../types";
|
||||||
@@ -18,9 +21,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@customElement("hui-heading-badge")
|
@customElement("hui-heading-badge")
|
||||||
export class HuiHeadingBadge extends ConditionalListenerMixin<LovelaceHeadingBadgeConfig>(
|
export class HuiHeadingBadge extends ConditionalListenerMixin(ReactiveElement) {
|
||||||
ReactiveElement
|
|
||||||
) {
|
|
||||||
@property({ type: Boolean }) public preview = false;
|
@property({ type: Boolean }) public preview = false;
|
||||||
|
|
||||||
@property({ attribute: false }) public config?: LovelaceHeadingBadgeConfig;
|
@property({ attribute: false }) public config?: LovelaceHeadingBadgeConfig;
|
||||||
@@ -51,7 +52,7 @@ export class HuiHeadingBadge extends ConditionalListenerMixin<LovelaceHeadingBad
|
|||||||
this._updateVisibility();
|
this._updateVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _updateElement(config: LovelaceHeadingBadgeConfig) {
|
private _updateElement(config: LovelaceHeadingBadgeConfig) {
|
||||||
if (!this._element) {
|
if (!this._element) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -132,7 +133,22 @@ export class HuiHeadingBadge extends ConditionalListenerMixin<LovelaceHeadingBad
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _updateVisibility(conditionsMet?: boolean) {
|
protected setupConditionalListeners() {
|
||||||
|
if (!this.config?.visibility || !this.hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupMediaQueryListeners(
|
||||||
|
this.config.visibility,
|
||||||
|
this.hass,
|
||||||
|
(unsub) => this.addConditionalListener(unsub),
|
||||||
|
(conditionsMet) => {
|
||||||
|
this._updateVisibility(conditionsMet);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateVisibility(forceVisible?: boolean) {
|
||||||
if (!this._element || !this.hass) {
|
if (!this._element || !this.hass) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -142,20 +158,11 @@ export class HuiHeadingBadge extends ConditionalListenerMixin<LovelaceHeadingBad
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.preview) {
|
|
||||||
this._setElementVisibility(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.config?.disabled) {
|
|
||||||
this._setElementVisibility(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const visible =
|
const visible =
|
||||||
conditionsMet ??
|
forceVisible ||
|
||||||
(!this.config?.visibility ||
|
this.preview ||
|
||||||
checkConditionsMet(this.config.visibility, this.hass));
|
!this.config?.visibility ||
|
||||||
|
checkConditionsMet(this.config.visibility, this.hass);
|
||||||
this._setElementVisibility(visible);
|
this._setElementVisibility(visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ import type {
|
|||||||
} from "../../../data/lovelace/config/section";
|
} from "../../../data/lovelace/config/section";
|
||||||
import { isStrategySection } from "../../../data/lovelace/config/section";
|
import { isStrategySection } from "../../../data/lovelace/config/section";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
|
import {
|
||||||
|
ConditionalListenerMixin,
|
||||||
|
setupMediaQueryListeners,
|
||||||
|
} from "../../../mixins/conditional-listener-mixin";
|
||||||
import "../cards/hui-card";
|
import "../cards/hui-card";
|
||||||
import type { HuiCard } from "../cards/hui-card";
|
import type { HuiCard } from "../cards/hui-card";
|
||||||
import { checkConditionsMet } from "../common/validate-condition";
|
import { checkConditionsMet } from "../common/validate-condition";
|
||||||
@@ -34,9 +37,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@customElement("hui-section")
|
@customElement("hui-section")
|
||||||
export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
export class HuiSection extends ConditionalListenerMixin(ReactiveElement) {
|
||||||
ReactiveElement
|
|
||||||
) {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public config!: LovelaceSectionRawConfig;
|
@property({ attribute: false }) public config!: LovelaceSectionRawConfig;
|
||||||
@@ -58,6 +59,8 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
|||||||
|
|
||||||
private _layoutElement?: LovelaceSectionElement;
|
private _layoutElement?: LovelaceSectionElement;
|
||||||
|
|
||||||
|
private _config: LovelaceSectionConfig | undefined;
|
||||||
|
|
||||||
@storage({
|
@storage({
|
||||||
key: "dashboardCardClipboard",
|
key: "dashboardCardClipboard",
|
||||||
state: false,
|
state: false,
|
||||||
@@ -113,7 +116,7 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
|||||||
|
|
||||||
public connectedCallback() {
|
public connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this._updateVisibility();
|
this._updateElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected update(changedProperties) {
|
protected update(changedProperties) {
|
||||||
@@ -144,11 +147,26 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
|||||||
this._layoutElement.cards = this._cards;
|
this._layoutElement.cards = this._cards;
|
||||||
}
|
}
|
||||||
if (changedProperties.has("hass") || changedProperties.has("preview")) {
|
if (changedProperties.has("hass") || changedProperties.has("preview")) {
|
||||||
this._updateVisibility();
|
this._updateElement();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected setupConditionalListeners() {
|
||||||
|
if (!this._config?.visibility || !this.hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupMediaQueryListeners(
|
||||||
|
this._config.visibility,
|
||||||
|
this.hass,
|
||||||
|
(unsub) => this.addConditionalListener(unsub),
|
||||||
|
(conditionsMet) => {
|
||||||
|
this._updateElement(conditionsMet);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private async _initializeConfig() {
|
private async _initializeConfig() {
|
||||||
let sectionConfig = { ...this.config };
|
let sectionConfig = { ...this.config };
|
||||||
let isStrategy = false;
|
let isStrategy = false;
|
||||||
@@ -190,11 +208,11 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
|||||||
while (this.lastChild) {
|
while (this.lastChild) {
|
||||||
this.removeChild(this.lastChild);
|
this.removeChild(this.lastChild);
|
||||||
}
|
}
|
||||||
this._updateVisibility();
|
this._updateElement();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _updateVisibility(conditionsMet?: boolean) {
|
private _updateElement(ignoreConditions?: boolean) {
|
||||||
if (!this._layoutElement || !this._config) {
|
if (!this._layoutElement || !this._config) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -210,9 +228,9 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const visible =
|
const visible =
|
||||||
conditionsMet ??
|
ignoreConditions ||
|
||||||
(!this._config.visibility ||
|
!this._config.visibility ||
|
||||||
checkConditionsMet(this._config.visibility, this.hass));
|
checkConditionsMet(this._config.visibility, this.hass);
|
||||||
|
|
||||||
this._setElementVisibility(visible);
|
this._setElementVisibility(visible);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,14 +14,12 @@ import memoizeOne from "memoize-one";
|
|||||||
import { relativeTime } from "../../common/datetime/relative_time";
|
import { relativeTime } from "../../common/datetime/relative_time";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import "../../components/ha-button";
|
import "../../components/ha-button";
|
||||||
|
import "../../components/ha-md-button-menu";
|
||||||
|
import "../../components/ha-md-menu-item";
|
||||||
import "../../components/ha-card";
|
import "../../components/ha-card";
|
||||||
import "../../components/ha-dropdown";
|
|
||||||
import "../../components/ha-dropdown-item";
|
|
||||||
import "../../components/ha-icon-button";
|
import "../../components/ha-icon-button";
|
||||||
import "../../components/ha-label";
|
import "../../components/ha-label";
|
||||||
import "../../components/ha-list-item";
|
import "../../components/ha-list-item";
|
||||||
import "../../components/ha-md-button-menu";
|
|
||||||
import "../../components/ha-md-menu-item";
|
|
||||||
import "../../components/ha-settings-row";
|
import "../../components/ha-settings-row";
|
||||||
import { deleteAllRefreshTokens } from "../../data/auth";
|
import { deleteAllRefreshTokens } from "../../data/auth";
|
||||||
import type { RefreshToken } from "../../data/refresh_token";
|
import type { RefreshToken } from "../../data/refresh_token";
|
||||||
@@ -148,18 +146,19 @@ class HaRefreshTokens extends LitElement {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<ha-dropdown @wa-select=${this._handleDropdownSelect}>
|
<ha-md-button-menu positioning="popover">
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
.label=${this.hass.localize("ui.common.menu")}
|
.label=${this.hass.localize("ui.common.menu")}
|
||||||
.path=${mdiDotsVertical}
|
.path=${mdiDotsVertical}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
<ha-dropdown-item
|
<ha-md-menu-item
|
||||||
|
graphic="icon"
|
||||||
|
@click=${this._toggleTokenExpiration}
|
||||||
.token=${token}
|
.token=${token}
|
||||||
.action=${"toggle_expiration"}
|
|
||||||
>
|
>
|
||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
slot="icon"
|
slot="start"
|
||||||
.path=${token.expire_at
|
.path=${token.expire_at
|
||||||
? mdiClockRemoveOutline
|
? mdiClockRemoveOutline
|
||||||
: mdiClockCheckOutline}
|
: mdiClockCheckOutline}
|
||||||
@@ -171,20 +170,24 @@ class HaRefreshTokens extends LitElement {
|
|||||||
: this.hass.localize(
|
: this.hass.localize(
|
||||||
"ui.panel.profile.refresh_tokens.enable_token_expiration"
|
"ui.panel.profile.refresh_tokens.enable_token_expiration"
|
||||||
)}
|
)}
|
||||||
</ha-dropdown-item>
|
</ha-md-menu-item>
|
||||||
<ha-dropdown-item
|
<ha-md-menu-item
|
||||||
.token=${token}
|
graphic="icon"
|
||||||
.action=${"delete_token"}
|
class="warning"
|
||||||
variant="danger"
|
|
||||||
.disabled=${token.is_current}
|
.disabled=${token.is_current}
|
||||||
|
@click=${this._deleteToken}
|
||||||
|
.token=${token}
|
||||||
>
|
>
|
||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
slot="icon"
|
class="warning"
|
||||||
|
slot="start"
|
||||||
.path=${mdiDelete}
|
.path=${mdiDelete}
|
||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
|
<div slot="headline">
|
||||||
${this.hass.localize("ui.common.delete")}
|
${this.hass.localize("ui.common.delete")}
|
||||||
</ha-dropdown-item>
|
</div>
|
||||||
</ha-dropdown>
|
</ha-md-menu-item>
|
||||||
|
</ha-md-button-menu>
|
||||||
</div>
|
</div>
|
||||||
</ha-settings-row>
|
</ha-settings-row>
|
||||||
`
|
`
|
||||||
@@ -207,17 +210,8 @@ class HaRefreshTokens extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleDropdownSelect(
|
private async _toggleTokenExpiration(ev): Promise<void> {
|
||||||
ev: CustomEvent<{ item: { action: string; token: RefreshToken } }>
|
const token = (ev.currentTarget as any).token as RefreshToken;
|
||||||
) {
|
|
||||||
if (ev.detail.item.action === "toggle_expiration") {
|
|
||||||
this._toggleTokenExpiration(ev.detail.item.token);
|
|
||||||
} else if (ev.detail.item.action === "delete_token") {
|
|
||||||
this._deleteToken(ev.detail.item.token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _toggleTokenExpiration(token: RefreshToken): Promise<void> {
|
|
||||||
const enable = !token.expire_at;
|
const enable = !token.expire_at;
|
||||||
if (!enable) {
|
if (!enable) {
|
||||||
if (
|
if (
|
||||||
@@ -258,7 +252,8 @@ class HaRefreshTokens extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _deleteToken(token: RefreshToken): Promise<void> {
|
private async _deleteToken(ev): Promise<void> {
|
||||||
|
const token = (ev.currentTarget as any).token as RefreshToken;
|
||||||
if (
|
if (
|
||||||
!(await showConfirmationDialog(this, {
|
!(await showConfirmationDialog(this, {
|
||||||
title: this.hass.localize(
|
title: this.hass.localize(
|
||||||
|
|||||||
@@ -199,23 +199,3 @@ export const baseEntrypointStyles = css`
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const baseAnimationStyles = css`
|
|
||||||
@keyframes fade-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fade-out {
|
|
||||||
from {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|||||||
@@ -74,10 +74,6 @@ export const coreColorStyles = css`
|
|||||||
--ha-color-green-80: #93da98;
|
--ha-color-green-80: #93da98;
|
||||||
--ha-color-green-90: #c2f2c1;
|
--ha-color-green-90: #c2f2c1;
|
||||||
--ha-color-green-95: #e3f9e3;
|
--ha-color-green-95: #e3f9e3;
|
||||||
|
|
||||||
/* shadow */
|
|
||||||
--ha-color-shadow-light: #00000014;
|
|
||||||
--ha-color-shadow-dark: #00000046;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -155,7 +155,6 @@ export const semanticColorStyles = css`
|
|||||||
|
|
||||||
/* Surfaces */
|
/* Surfaces */
|
||||||
--ha-color-surface-default: var(--ha-color-neutral-95);
|
--ha-color-surface-default: var(--ha-color-neutral-95);
|
||||||
--ha-color-on-surface-default: var(--ha-color-neutral-05);
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -287,6 +286,5 @@ export const darkSemanticColorStyles = css`
|
|||||||
|
|
||||||
/* Surfaces */
|
/* Surfaces */
|
||||||
--ha-color-surface-default: var(--ha-color-neutral-10);
|
--ha-color-surface-default: var(--ha-color-neutral-10);
|
||||||
--ha-color-on-surface-default: var(--ha-color-neutral-95);
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -52,9 +52,7 @@ export const waColorStyles = css`
|
|||||||
--wa-color-danger-on-normal: var(--ha-color-on-danger-normal);
|
--wa-color-danger-on-normal: var(--ha-color-on-danger-normal);
|
||||||
--wa-color-danger-on-quiet: var(--ha-color-on-danger-quiet);
|
--wa-color-danger-on-quiet: var(--ha-color-on-danger-quiet);
|
||||||
|
|
||||||
--wa-color-text-normal: var(--ha-color-text-primary);
|
|
||||||
--wa-color-surface-default: var(--card-background-color);
|
--wa-color-surface-default: var(--card-background-color);
|
||||||
--wa-color-surface-raised: var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff));
|
|
||||||
--wa-panel-border-radius: var(--ha-border-radius-3xl);
|
--wa-panel-border-radius: var(--ha-border-radius-3xl);
|
||||||
--wa-panel-border-style: solid;
|
--wa-panel-border-style: solid;
|
||||||
--wa-panel-border-width: 1px;
|
--wa-panel-border-width: 1px;
|
||||||
|
|||||||
@@ -42,26 +42,6 @@ export const coreStyles = css`
|
|||||||
--ha-space-18: 72px;
|
--ha-space-18: 72px;
|
||||||
--ha-space-19: 76px;
|
--ha-space-19: 76px;
|
||||||
--ha-space-20: 80px;
|
--ha-space-20: 80px;
|
||||||
|
|
||||||
--ha-shadow-offset-x-sm: 0;
|
|
||||||
--ha-shadow-offset-x-md: 0;
|
|
||||||
--ha-shadow-offset-x-lg: 0;
|
|
||||||
--ha-shadow-offset-y-sm: 2px;
|
|
||||||
--ha-shadow-offset-y-md: 4px;
|
|
||||||
--ha-shadow-offset-y-lg: 8px;
|
|
||||||
--ha-shadow-blur-sm: 4px;
|
|
||||||
--ha-shadow-blur-md: 8px;
|
|
||||||
--ha-shadow-blur-lg: 12px;
|
|
||||||
--ha-shadow-spread-sm: 0;
|
|
||||||
--ha-shadow-spread-md: 0;
|
|
||||||
--ha-shadow-spread-lg: 0;
|
|
||||||
--ha-animation-base-duration: 350ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
html {
|
|
||||||
--ha-animation-base-duration: 0ms;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import { css } from "lit";
|
|
||||||
import { extractVars } from "../../common/style/derived-css-vars";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Semantic styles use core styles to define higher level variables like box shadows.
|
|
||||||
* Here we define all styles except colors
|
|
||||||
*/
|
|
||||||
export const semanticStyles = css`
|
|
||||||
html {
|
|
||||||
--ha-box-shadow-s: var(--ha-shadow-offset-x-sm) var(--ha-shadow-offset-y-sm) var(--ha-shadow-blur-sm) var(--ha-shadow-spread-sm) var(--ha-color-shadow-light);
|
|
||||||
--ha-box-shadow-m: var(--ha-shadow-offset-x-md) var(--ha-shadow-offset-y-md) var(--ha-shadow-blur-md) var(--ha-shadow-spread-md) var(--ha-color-shadow-light);
|
|
||||||
--ha-box-shadow-l: var(--ha-shadow-offset-x-lg) var(--ha-shadow-offset-y-lg) var(--ha-shadow-blur-lg) var(--ha-shadow-spread-lg) var(--ha-color-shadow-light);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const darkSemanticStyles = css`
|
|
||||||
html {
|
|
||||||
--ha-box-shadow-s: var(--ha-shadow-offset-x-sm) var(--ha-shadow-offset-y-sm) var(--ha-shadow-blur-sm) var(--ha-shadow-spread-sm) var(--ha-color-shadow-dark);
|
|
||||||
--ha-box-shadow-m: var(--ha-shadow-offset-x-md) var(--ha-shadow-offset-y-md) var(--ha-shadow-blur-md) var(--ha-shadow-spread-md) var(--ha-color-shadow-dark);
|
|
||||||
--ha-box-shadow-l: var(--ha-shadow-offset-x-lg) var(--ha-shadow-offset-y-lg) var(--ha-shadow-blur-lg) var(--ha-shadow-spread-lg) var(--ha-color-shadow-dark);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const darkSemanticVariables = extractVars(darkSemanticStyles);
|
|
||||||
@@ -2,7 +2,6 @@ import { fontStyles } from "../roboto";
|
|||||||
import { colorDerivedVariables, colorStylesCollection } from "./color";
|
import { colorDerivedVariables, colorStylesCollection } from "./color";
|
||||||
import { coreDerivedVariables, coreStyles } from "./core.globals";
|
import { coreDerivedVariables, coreStyles } from "./core.globals";
|
||||||
import { mainDerivedVariables, mainStyles } from "./main.globals";
|
import { mainDerivedVariables, mainStyles } from "./main.globals";
|
||||||
import { semanticStyles } from "./semantic.globals";
|
|
||||||
import {
|
import {
|
||||||
typographyDerivedVariables,
|
typographyDerivedVariables,
|
||||||
typographyStyles,
|
typographyStyles,
|
||||||
@@ -13,7 +12,6 @@ export const themeStyles = [
|
|||||||
coreStyles.toString(),
|
coreStyles.toString(),
|
||||||
mainStyles.toString(),
|
mainStyles.toString(),
|
||||||
typographyStyles.toString(),
|
typographyStyles.toString(),
|
||||||
semanticStyles.toString(),
|
|
||||||
...colorStylesCollection,
|
...colorStylesCollection,
|
||||||
fontStyles.toString(),
|
fontStyles.toString(),
|
||||||
waMainStyles.toString(),
|
waMainStyles.toString(),
|
||||||
|
|||||||
@@ -9,28 +9,15 @@ export const waMainStyles = css`
|
|||||||
--wa-focus-ring-offset: 2px;
|
--wa-focus-ring-offset: 2px;
|
||||||
--wa-focus-ring: var(--wa-focus-ring-style) var(--wa-focus-ring-width) var(--wa-focus-ring-color);
|
--wa-focus-ring: var(--wa-focus-ring-style) var(--wa-focus-ring-width) var(--wa-focus-ring-color);
|
||||||
|
|
||||||
--wa-space-l: var(--ha-space-6);
|
--wa-space-l: 24px;
|
||||||
--wa-space-xl: var(--ha-space-8);
|
--wa-shadow-l: 0 8px 8px -4px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
--wa-form-control-padding-block: 0.75em;
|
--wa-form-control-padding-block: 0.75em;
|
||||||
|
|
||||||
--wa-font-weight-action: var(--ha-font-weight-medium);
|
--wa-font-weight-action: var(--ha-font-weight-medium);
|
||||||
--wa-transition-fast: 75ms;
|
--wa-transition-fast: 75ms;
|
||||||
--wa-transition-easing: ease;
|
--wa-transition-easing: ease;
|
||||||
|
--wa-border-width-l: var(--ha-border-radius-lg);
|
||||||
--wa-border-style: solid;
|
--wa-space-xl: 32px;
|
||||||
--wa-border-width-s: var(--ha-border-width-sm);
|
|
||||||
--wa-border-width-m: var(--ha-border-width-md);
|
|
||||||
--wa-border-width-l: var(--ha-border-width-lg);
|
|
||||||
--wa-border-radius-s: var(--ha-border-radius-sm);
|
|
||||||
--wa-border-radius-m: var(--ha-border-radius-md);
|
|
||||||
--wa-border-radius-l: var(--ha-border-radius-lg);
|
|
||||||
|
|
||||||
--wa-line-height-condensed: var(--ha-line-height-condensed);
|
|
||||||
|
|
||||||
--wa-shadow-s: var(--ha-box-shadow-s);
|
|
||||||
--wa-shadow-m: var(--ha-box-shadow-m);
|
|
||||||
--wa-shadow-l: var(--ha-box-shadow-l);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
${scrollLockStyles}
|
${scrollLockStyles}
|
||||||
|
|||||||
@@ -13,8 +13,7 @@
|
|||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
"light": "Lights",
|
"light": "Lights",
|
||||||
"security": "Security",
|
"security": "Security",
|
||||||
"climate": "Climate",
|
"climate": "Climate"
|
||||||
"home": "Home"
|
|
||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"default": {
|
"default": {
|
||||||
@@ -3077,15 +3076,6 @@
|
|||||||
"grid_carbon_footprint": "Grid carbon footprint",
|
"grid_carbon_footprint": "Grid carbon footprint",
|
||||||
"remove_co2_signal": "Remove Electricity Maps integration",
|
"remove_co2_signal": "Remove Electricity Maps integration",
|
||||||
"add_co2_signal": "Add Electricity Maps integration",
|
"add_co2_signal": "Add Electricity Maps integration",
|
||||||
"grid_power": "Grid power",
|
|
||||||
"add_power": "Add power sensor",
|
|
||||||
"edit_power": "Edit power sensor",
|
|
||||||
"delete_power": "Delete power sensor",
|
|
||||||
"power_dialog": {
|
|
||||||
"header": "Configure grid power",
|
|
||||||
"power_stat": "Power sensor",
|
|
||||||
"power_helper": "Pick a sensor which measures grid power in either of {unit}. Positive values indicate importing electricity from the grid, negative values indicate exporting electricity to the grid."
|
|
||||||
},
|
|
||||||
"flow_dialog": {
|
"flow_dialog": {
|
||||||
"cost_entity_helper": "Any sensor with a unit of `{currency}/(valid energy unit)` (e.g. `{currency}/Wh` or `{currency}/kWh`) may be used and will be automatically converted.",
|
"cost_entity_helper": "Any sensor with a unit of `{currency}/(valid energy unit)` (e.g. `{currency}/Wh` or `{currency}/kWh`) may be used and will be automatically converted.",
|
||||||
"from": {
|
"from": {
|
||||||
@@ -3133,7 +3123,6 @@
|
|||||||
"header": "Configure solar panels",
|
"header": "Configure solar panels",
|
||||||
"entity_para": "Pick a sensor which measures solar energy production in either of {unit}.",
|
"entity_para": "Pick a sensor which measures solar energy production in either of {unit}.",
|
||||||
"solar_production_energy": "Solar production energy",
|
"solar_production_energy": "Solar production energy",
|
||||||
"solar_production_power": "Solar production power",
|
|
||||||
"solar_production_forecast": "Solar production forecast",
|
"solar_production_forecast": "Solar production forecast",
|
||||||
"solar_production_forecast_description": "Adding solar production forecast information will allow you to quickly see your expected production for today.",
|
"solar_production_forecast_description": "Adding solar production forecast information will allow you to quickly see your expected production for today.",
|
||||||
"dont_forecast_production": "Don't forecast production",
|
"dont_forecast_production": "Don't forecast production",
|
||||||
@@ -3151,12 +3140,9 @@
|
|||||||
"add_battery_system": "Add battery system",
|
"add_battery_system": "Add battery system",
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"header": "Configure battery system",
|
"header": "Configure battery system",
|
||||||
"energy_helper_into": "Pick a sensor that measures the electricity flowing into the battery in either of {unit}.",
|
"entity_para": "Pick sensors which measure energy going into and coming out of the battery in either of {unit}.",
|
||||||
"energy_helper_out": "Pick a sensor that measures the electricity flowing out of the battery in either of {unit}.",
|
"energy_into_battery": "Energy going into the battery",
|
||||||
"energy_into_battery": "Energy charged into the battery",
|
"energy_out_of_battery": "Energy coming out of the battery"
|
||||||
"energy_out_of_battery": "Energy discharged from the battery",
|
|
||||||
"power": "Battery power",
|
|
||||||
"power_helper": "Pick a sensor which measures the electricity flowing into and out of the battery in either of {unit}. Positive values indicate discharging the battery, negative values indicate charging the battery."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"gas": {
|
"gas": {
|
||||||
@@ -3218,8 +3204,7 @@
|
|||||||
"header": "Add a device",
|
"header": "Add a device",
|
||||||
"display_name": "Display name",
|
"display_name": "Display name",
|
||||||
"device_consumption_energy": "Device energy consumption",
|
"device_consumption_energy": "Device energy consumption",
|
||||||
"device_consumption_power": "Device power consumption",
|
"selected_stat_intro": "Select the energy sensor that measures the device's energy usage in either of {unit}.",
|
||||||
"selected_stat_intro": "Select the sensor that measures the device's electricity usage in either of {unit}.",
|
|
||||||
"included_in_device": "Upstream device",
|
"included_in_device": "Upstream device",
|
||||||
"included_in_device_helper": "If this device is already counted by another device (such as a smart switch measured by a smart breaker), selecting the upstream device prevents duplicate energy tracking.",
|
"included_in_device_helper": "If this device is already counted by another device (such as a smart switch measured by a smart breaker), selecting the upstream device prevents duplicate energy tracking.",
|
||||||
"no_upstream_devices": "No eligible upstream devices"
|
"no_upstream_devices": "No eligible upstream devices"
|
||||||
@@ -7578,12 +7563,6 @@
|
|||||||
"state_equal": "State is equal to",
|
"state_equal": "State is equal to",
|
||||||
"state_not_equal": "State is not equal to"
|
"state_not_equal": "State is not equal to"
|
||||||
},
|
},
|
||||||
"time": {
|
|
||||||
"label": "Time",
|
|
||||||
"after": "After",
|
|
||||||
"before": "Before",
|
|
||||||
"weekdays": "Weekdays"
|
|
||||||
},
|
|
||||||
"location": {
|
"location": {
|
||||||
"label": "Location",
|
"label": "Location",
|
||||||
"locations": "Locations",
|
"locations": "Locations",
|
||||||
|
|||||||
@@ -1,40 +1,10 @@
|
|||||||
import type { TemplateResult } from "lit";
|
import type { TemplateResult } from "lit";
|
||||||
import { render } from "lit";
|
import { render } from "lit";
|
||||||
import { parseAnimationDuration } from "../common/util/parse-animation-duration";
|
|
||||||
|
|
||||||
const removeElement = (
|
|
||||||
launchScreenElement: HTMLElement,
|
|
||||||
skipAnimation: boolean
|
|
||||||
) => {
|
|
||||||
if (skipAnimation) {
|
|
||||||
launchScreenElement.parentElement?.removeChild(launchScreenElement);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
launchScreenElement.classList.add("removing");
|
|
||||||
|
|
||||||
const durationFromCss = getComputedStyle(document.documentElement)
|
|
||||||
.getPropertyValue("--ha-animation-base-duration")
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
launchScreenElement.parentElement?.removeChild(launchScreenElement);
|
|
||||||
}, parseAnimationDuration(durationFromCss));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeLaunchScreen = () => {
|
export const removeLaunchScreen = () => {
|
||||||
const launchScreenElement = document.getElementById("ha-launch-screen");
|
const launchScreenElement = document.getElementById("ha-launch-screen");
|
||||||
if (!launchScreenElement?.parentElement) {
|
if (launchScreenElement) {
|
||||||
return;
|
launchScreenElement.parentElement!.removeChild(launchScreenElement);
|
||||||
}
|
|
||||||
|
|
||||||
if (document.startViewTransition) {
|
|
||||||
document.startViewTransition(() => {
|
|
||||||
removeElement(launchScreenElement, false);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Fallback: Direct removal without transition
|
|
||||||
removeElement(launchScreenElement, true);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,437 +0,0 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
|
||||||
import {
|
|
||||||
extractMediaQueries,
|
|
||||||
extractTimeConditions,
|
|
||||||
} from "../../../src/common/condition/extract";
|
|
||||||
import type {
|
|
||||||
Condition,
|
|
||||||
TimeCondition,
|
|
||||||
ScreenCondition,
|
|
||||||
OrCondition,
|
|
||||||
AndCondition,
|
|
||||||
NotCondition,
|
|
||||||
} from "../../../src/panels/lovelace/common/validate-condition";
|
|
||||||
|
|
||||||
describe("extractMediaQueries", () => {
|
|
||||||
it("should extract single media query", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "screen",
|
|
||||||
media_query: "(max-width: 600px)",
|
|
||||||
} as ScreenCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = extractMediaQueries(conditions);
|
|
||||||
|
|
||||||
expect(result).toEqual(["(max-width: 600px)"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should extract multiple media queries", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "screen",
|
|
||||||
media_query: "(max-width: 600px)",
|
|
||||||
} as ScreenCondition,
|
|
||||||
{
|
|
||||||
condition: "screen",
|
|
||||||
media_query: "(min-width: 1200px)",
|
|
||||||
} as ScreenCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = extractMediaQueries(conditions);
|
|
||||||
|
|
||||||
expect(result).toEqual(["(max-width: 600px)", "(min-width: 1200px)"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return empty array when no screen conditions", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
} as TimeCondition,
|
|
||||||
{
|
|
||||||
condition: "state",
|
|
||||||
entity: "light.living_room",
|
|
||||||
state: "on",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = extractMediaQueries(conditions);
|
|
||||||
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should ignore screen conditions without media_query", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "screen",
|
|
||||||
} as ScreenCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = extractMediaQueries(conditions);
|
|
||||||
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should extract from nested or conditions", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "or",
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
condition: "screen",
|
|
||||||
media_query: "(max-width: 600px)",
|
|
||||||
} as ScreenCondition,
|
|
||||||
{
|
|
||||||
condition: "state",
|
|
||||||
entity: "light.living_room",
|
|
||||||
state: "on",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as OrCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = extractMediaQueries(conditions);
|
|
||||||
|
|
||||||
expect(result).toEqual(["(max-width: 600px)"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should extract from nested and conditions", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "and",
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
condition: "screen",
|
|
||||||
media_query: "(orientation: portrait)",
|
|
||||||
} as ScreenCondition,
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
} as TimeCondition,
|
|
||||||
],
|
|
||||||
} as AndCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = extractMediaQueries(conditions);
|
|
||||||
|
|
||||||
expect(result).toEqual(["(orientation: portrait)"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should extract from nested not conditions", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "not",
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
condition: "screen",
|
|
||||||
media_query: "(prefers-color-scheme: dark)",
|
|
||||||
} as ScreenCondition,
|
|
||||||
],
|
|
||||||
} as NotCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = extractMediaQueries(conditions);
|
|
||||||
|
|
||||||
expect(result).toEqual(["(prefers-color-scheme: dark)"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should extract from deeply nested conditions", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "or",
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
condition: "and",
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
condition: "screen",
|
|
||||||
media_query: "(max-width: 600px)",
|
|
||||||
} as ScreenCondition,
|
|
||||||
{
|
|
||||||
condition: "not",
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
condition: "screen",
|
|
||||||
media_query: "(orientation: landscape)",
|
|
||||||
} as ScreenCondition,
|
|
||||||
],
|
|
||||||
} as NotCondition,
|
|
||||||
],
|
|
||||||
} as AndCondition,
|
|
||||||
],
|
|
||||||
} as OrCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = extractMediaQueries(conditions);
|
|
||||||
|
|
||||||
expect(result).toEqual(["(max-width: 600px)", "(orientation: landscape)"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle empty conditions array", () => {
|
|
||||||
const result = extractMediaQueries([]);
|
|
||||||
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle mixed conditions with nesting", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "screen",
|
|
||||||
media_query: "(max-width: 600px)",
|
|
||||||
} as ScreenCondition,
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
} as TimeCondition,
|
|
||||||
{
|
|
||||||
condition: "or",
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
condition: "screen",
|
|
||||||
media_query: "(min-width: 1200px)",
|
|
||||||
} as ScreenCondition,
|
|
||||||
],
|
|
||||||
} as OrCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = extractMediaQueries(conditions);
|
|
||||||
|
|
||||||
expect(result).toEqual(["(max-width: 600px)", "(min-width: 1200px)"]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("extractTimeConditions", () => {
|
|
||||||
it("should extract single time condition", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
before: "17:00",
|
|
||||||
} as TimeCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = extractTimeConditions(conditions);
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
before: "17:00",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should extract multiple time conditions", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
} as TimeCondition,
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
before: "17:00",
|
|
||||||
weekdays: ["mon", "tue", "wed", "thu", "fri"],
|
|
||||||
} as TimeCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = extractTimeConditions(conditions);
|
|
||||||
|
|
||||||
expect(result).toHaveLength(2);
|
|
||||||
expect(result[0]).toMatchObject({ condition: "time", after: "08:00" });
|
|
||||||
expect(result[1]).toMatchObject({
|
|
||||||
condition: "time",
|
|
||||||
before: "17:00",
|
|
||||||
weekdays: ["mon", "tue", "wed", "thu", "fri"],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return empty array when no time conditions", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "screen",
|
|
||||||
media_query: "(max-width: 600px)",
|
|
||||||
} as ScreenCondition,
|
|
||||||
{
|
|
||||||
condition: "state",
|
|
||||||
entity: "light.living_room",
|
|
||||||
state: "on",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = extractTimeConditions(conditions);
|
|
||||||
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should extract from nested or conditions", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "or",
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
} as TimeCondition,
|
|
||||||
{
|
|
||||||
condition: "state",
|
|
||||||
entity: "light.living_room",
|
|
||||||
state: "on",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as OrCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = extractTimeConditions(conditions);
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should extract from nested and conditions", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "and",
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
condition: "screen",
|
|
||||||
media_query: "(max-width: 600px)",
|
|
||||||
} as ScreenCondition,
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
weekdays: ["sat", "sun"],
|
|
||||||
} as TimeCondition,
|
|
||||||
],
|
|
||||||
} as AndCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = extractTimeConditions(conditions);
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
weekdays: ["sat", "sun"],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should extract from nested not conditions", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "not",
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
after: "22:00",
|
|
||||||
before: "06:00",
|
|
||||||
} as TimeCondition,
|
|
||||||
],
|
|
||||||
} as NotCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = extractTimeConditions(conditions);
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
after: "22:00",
|
|
||||||
before: "06:00",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should extract from deeply nested conditions", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "or",
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
condition: "and",
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
} as TimeCondition,
|
|
||||||
{
|
|
||||||
condition: "not",
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
weekdays: ["sat", "sun"],
|
|
||||||
} as TimeCondition,
|
|
||||||
],
|
|
||||||
} as NotCondition,
|
|
||||||
],
|
|
||||||
} as AndCondition,
|
|
||||||
],
|
|
||||||
} as OrCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = extractTimeConditions(conditions);
|
|
||||||
|
|
||||||
expect(result).toHaveLength(2);
|
|
||||||
expect(result[0]).toMatchObject({ condition: "time", after: "08:00" });
|
|
||||||
expect(result[1]).toMatchObject({
|
|
||||||
condition: "time",
|
|
||||||
weekdays: ["sat", "sun"],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle empty conditions array", () => {
|
|
||||||
const result = extractTimeConditions([]);
|
|
||||||
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle mixed conditions with nesting", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
} as TimeCondition,
|
|
||||||
{
|
|
||||||
condition: "screen",
|
|
||||||
media_query: "(max-width: 600px)",
|
|
||||||
} as ScreenCondition,
|
|
||||||
{
|
|
||||||
condition: "or",
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
before: "22:00",
|
|
||||||
} as TimeCondition,
|
|
||||||
],
|
|
||||||
} as OrCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = extractTimeConditions(conditions);
|
|
||||||
|
|
||||||
expect(result).toHaveLength(2);
|
|
||||||
expect(result[0]).toMatchObject({ condition: "time", after: "08:00" });
|
|
||||||
expect(result[1]).toMatchObject({ condition: "time", before: "22:00" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should preserve all time condition properties", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
before: "17:00",
|
|
||||||
weekdays: ["mon", "tue", "wed", "thu", "fri"],
|
|
||||||
} as TimeCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = extractTimeConditions(conditions);
|
|
||||||
|
|
||||||
expect(result[0]).toEqual({
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
before: "17:00",
|
|
||||||
weekdays: ["mon", "tue", "wed", "thu", "fri"],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,519 +0,0 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
||||||
import {
|
|
||||||
setupTimeListeners,
|
|
||||||
setupMediaQueryListeners,
|
|
||||||
} from "../../../src/common/condition/listeners";
|
|
||||||
import * as timeCalculator from "../../../src/common/condition/time-calculator";
|
|
||||||
import type {
|
|
||||||
TimeCondition,
|
|
||||||
ScreenCondition,
|
|
||||||
Condition,
|
|
||||||
} from "../../../src/panels/lovelace/common/validate-condition";
|
|
||||||
import type { HomeAssistant } from "../../../src/types";
|
|
||||||
import * as mediaQuery from "../../../src/common/dom/media_query";
|
|
||||||
|
|
||||||
// Maximum delay for setTimeout (2^31 - 1 milliseconds, ~24.8 days)
|
|
||||||
const MAX_TIMEOUT_DELAY = 2147483647;
|
|
||||||
|
|
||||||
describe("setupTimeListeners", () => {
|
|
||||||
let hass: HomeAssistant;
|
|
||||||
let listeners: (() => void)[];
|
|
||||||
let onUpdateCallback: (conditionsMet: boolean) => void;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
listeners = [];
|
|
||||||
onUpdateCallback = vi.fn();
|
|
||||||
|
|
||||||
hass = {
|
|
||||||
locale: {
|
|
||||||
time_zone: "local",
|
|
||||||
},
|
|
||||||
config: {
|
|
||||||
time_zone: "America/New_York",
|
|
||||||
},
|
|
||||||
} as HomeAssistant;
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
listeners.forEach((unsub) => unsub());
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("setTimeout overflow protection", () => {
|
|
||||||
it("should cap delay at MAX_TIMEOUT_DELAY", () => {
|
|
||||||
const setTimeoutSpy = vi.spyOn(global, "setTimeout");
|
|
||||||
|
|
||||||
// Mock calculateNextTimeUpdate to return a delay exceeding the max
|
|
||||||
vi.spyOn(timeCalculator, "calculateNextTimeUpdate").mockReturnValue(
|
|
||||||
MAX_TIMEOUT_DELAY + 1000000
|
|
||||||
);
|
|
||||||
|
|
||||||
const conditions: TimeCondition[] = [
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
setupTimeListeners(
|
|
||||||
conditions,
|
|
||||||
hass,
|
|
||||||
(unsub) => listeners.push(unsub),
|
|
||||||
onUpdateCallback
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify setTimeout was called with the capped delay
|
|
||||||
expect(setTimeoutSpy).toHaveBeenCalledWith(
|
|
||||||
expect.any(Function),
|
|
||||||
MAX_TIMEOUT_DELAY
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not call onUpdate when hitting the cap", () => {
|
|
||||||
// Mock calculateNextTimeUpdate to return delays that decrease over time
|
|
||||||
// Both first and second delays exceed the cap
|
|
||||||
const delays = [
|
|
||||||
MAX_TIMEOUT_DELAY + 1000000,
|
|
||||||
MAX_TIMEOUT_DELAY + 500000,
|
|
||||||
1000,
|
|
||||||
];
|
|
||||||
let callCount = 0;
|
|
||||||
|
|
||||||
vi.spyOn(timeCalculator, "calculateNextTimeUpdate").mockImplementation(
|
|
||||||
() => delays[callCount++]
|
|
||||||
);
|
|
||||||
|
|
||||||
const conditions: TimeCondition[] = [
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
setupTimeListeners(
|
|
||||||
conditions,
|
|
||||||
hass,
|
|
||||||
(unsub) => listeners.push(unsub),
|
|
||||||
onUpdateCallback
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fast-forward to when the first timeout fires (at the cap)
|
|
||||||
vi.advanceTimersByTime(MAX_TIMEOUT_DELAY);
|
|
||||||
|
|
||||||
// onUpdate should NOT have been called because we hit the cap
|
|
||||||
expect(onUpdateCallback).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
// Fast-forward to the second timeout (still exceeds cap)
|
|
||||||
vi.advanceTimersByTime(MAX_TIMEOUT_DELAY);
|
|
||||||
|
|
||||||
// Still should not have been called
|
|
||||||
expect(onUpdateCallback).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
// Fast-forward to the third timeout (within cap)
|
|
||||||
vi.advanceTimersByTime(1000);
|
|
||||||
|
|
||||||
// NOW onUpdate should have been called
|
|
||||||
expect(onUpdateCallback).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call onUpdate normally when delay is within cap", () => {
|
|
||||||
const normalDelay = 5000; // 5 seconds
|
|
||||||
|
|
||||||
vi.spyOn(timeCalculator, "calculateNextTimeUpdate").mockReturnValue(
|
|
||||||
normalDelay
|
|
||||||
);
|
|
||||||
|
|
||||||
const conditions: TimeCondition[] = [
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
setupTimeListeners(
|
|
||||||
conditions,
|
|
||||||
hass,
|
|
||||||
(unsub) => listeners.push(unsub),
|
|
||||||
onUpdateCallback
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fast-forward by the normal delay
|
|
||||||
vi.advanceTimersByTime(normalDelay);
|
|
||||||
|
|
||||||
// onUpdate should have been called
|
|
||||||
expect(onUpdateCallback).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reschedule after hitting the cap", () => {
|
|
||||||
const setTimeoutSpy = vi.spyOn(global, "setTimeout");
|
|
||||||
|
|
||||||
// First delay exceeds cap, second delay is normal
|
|
||||||
const delays = [MAX_TIMEOUT_DELAY + 1000000, 5000];
|
|
||||||
let callCount = 0;
|
|
||||||
|
|
||||||
vi.spyOn(timeCalculator, "calculateNextTimeUpdate").mockImplementation(
|
|
||||||
() => delays[callCount++]
|
|
||||||
);
|
|
||||||
|
|
||||||
const conditions: TimeCondition[] = [
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
setupTimeListeners(
|
|
||||||
conditions,
|
|
||||||
hass,
|
|
||||||
(unsub) => listeners.push(unsub),
|
|
||||||
onUpdateCallback
|
|
||||||
);
|
|
||||||
|
|
||||||
// First setTimeout call should use the capped delay
|
|
||||||
expect(setTimeoutSpy).toHaveBeenNthCalledWith(
|
|
||||||
1,
|
|
||||||
expect.any(Function),
|
|
||||||
MAX_TIMEOUT_DELAY
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fast-forward to when the first timeout fires
|
|
||||||
vi.advanceTimersByTime(MAX_TIMEOUT_DELAY);
|
|
||||||
|
|
||||||
// Second setTimeout call should use the normal delay
|
|
||||||
expect(setTimeoutSpy).toHaveBeenNthCalledWith(
|
|
||||||
2,
|
|
||||||
expect.any(Function),
|
|
||||||
5000
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("listener cleanup", () => {
|
|
||||||
it("should register cleanup function for each time condition", () => {
|
|
||||||
const normalDelay = 5000;
|
|
||||||
|
|
||||||
vi.spyOn(timeCalculator, "calculateNextTimeUpdate").mockReturnValue(
|
|
||||||
normalDelay
|
|
||||||
);
|
|
||||||
|
|
||||||
const conditions: TimeCondition[] = [
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
before: "17:00",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
setupTimeListeners(
|
|
||||||
conditions,
|
|
||||||
hass,
|
|
||||||
(unsub) => listeners.push(unsub),
|
|
||||||
onUpdateCallback
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should have registered 2 cleanup functions (one per time condition)
|
|
||||||
expect(listeners).toHaveLength(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should clear timeout when cleanup is called", () => {
|
|
||||||
const clearTimeoutSpy = vi.spyOn(global, "clearTimeout");
|
|
||||||
const normalDelay = 5000;
|
|
||||||
|
|
||||||
vi.spyOn(timeCalculator, "calculateNextTimeUpdate").mockReturnValue(
|
|
||||||
normalDelay
|
|
||||||
);
|
|
||||||
|
|
||||||
const conditions: TimeCondition[] = [
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
setupTimeListeners(
|
|
||||||
conditions,
|
|
||||||
hass,
|
|
||||||
(unsub) => listeners.push(unsub),
|
|
||||||
onUpdateCallback
|
|
||||||
);
|
|
||||||
|
|
||||||
// Call cleanup
|
|
||||||
listeners[0]();
|
|
||||||
|
|
||||||
// Should have cleared the timeout
|
|
||||||
expect(clearTimeoutSpy).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("no time conditions", () => {
|
|
||||||
it("should not setup listeners when no time conditions exist", () => {
|
|
||||||
const setTimeoutSpy = vi.spyOn(global, "setTimeout");
|
|
||||||
|
|
||||||
setupTimeListeners(
|
|
||||||
[],
|
|
||||||
hass,
|
|
||||||
(unsub) => listeners.push(unsub),
|
|
||||||
onUpdateCallback
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should not have called setTimeout
|
|
||||||
expect(setTimeoutSpy).not.toHaveBeenCalled();
|
|
||||||
expect(listeners).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("undefined delay handling", () => {
|
|
||||||
it("should not setup timeout when calculateNextTimeUpdate returns undefined", () => {
|
|
||||||
const setTimeoutSpy = vi.spyOn(global, "setTimeout");
|
|
||||||
|
|
||||||
vi.spyOn(timeCalculator, "calculateNextTimeUpdate").mockReturnValue(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
const conditions: TimeCondition[] = [
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
weekdays: ["mon", "tue", "wed", "thu", "fri", "sat", "sun"],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
setupTimeListeners(
|
|
||||||
conditions,
|
|
||||||
hass,
|
|
||||||
(unsub) => listeners.push(unsub),
|
|
||||||
onUpdateCallback
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should not have called setTimeout
|
|
||||||
expect(setTimeoutSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("setupMediaQueryListeners", () => {
|
|
||||||
let hass: HomeAssistant;
|
|
||||||
let listeners: (() => void)[];
|
|
||||||
let onUpdateCallback: (conditionsMet: boolean) => void;
|
|
||||||
let listenMediaQuerySpy: any;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
listeners = [];
|
|
||||||
onUpdateCallback = vi.fn();
|
|
||||||
|
|
||||||
hass = {
|
|
||||||
locale: {
|
|
||||||
time_zone: "local",
|
|
||||||
},
|
|
||||||
config: {
|
|
||||||
time_zone: "America/New_York",
|
|
||||||
},
|
|
||||||
} as HomeAssistant;
|
|
||||||
|
|
||||||
// Mock matchMedia for screen condition checks
|
|
||||||
global.matchMedia = vi.fn().mockImplementation((query) => ({
|
|
||||||
matches: false,
|
|
||||||
media: query,
|
|
||||||
onchange: null,
|
|
||||||
addListener: vi.fn(),
|
|
||||||
removeListener: vi.fn(),
|
|
||||||
addEventListener: vi.fn(),
|
|
||||||
removeEventListener: vi.fn(),
|
|
||||||
dispatchEvent: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock listenMediaQuery to capture the callback
|
|
||||||
listenMediaQuerySpy = vi
|
|
||||||
.spyOn(mediaQuery, "listenMediaQuery")
|
|
||||||
.mockImplementation((_query, _callback) => vi.fn());
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
listeners.forEach((unsub) => unsub());
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("single media query", () => {
|
|
||||||
it("should setup listener for single screen condition", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "screen",
|
|
||||||
media_query: "(max-width: 600px)",
|
|
||||||
} as ScreenCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
setupMediaQueryListeners(
|
|
||||||
conditions,
|
|
||||||
hass,
|
|
||||||
(unsub) => listeners.push(unsub),
|
|
||||||
onUpdateCallback
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(listenMediaQuerySpy).toHaveBeenCalledWith(
|
|
||||||
"(max-width: 600px)",
|
|
||||||
expect.any(Function)
|
|
||||||
);
|
|
||||||
expect(listeners).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call onUpdate with matches value for single screen condition", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "screen",
|
|
||||||
media_query: "(max-width: 600px)",
|
|
||||||
} as ScreenCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
let capturedCallback: ((matches: boolean) => void) | undefined;
|
|
||||||
|
|
||||||
listenMediaQuerySpy.mockImplementation((_query, callback) => {
|
|
||||||
capturedCallback = callback;
|
|
||||||
return vi.fn();
|
|
||||||
});
|
|
||||||
|
|
||||||
setupMediaQueryListeners(
|
|
||||||
conditions,
|
|
||||||
hass,
|
|
||||||
(unsub) => listeners.push(unsub),
|
|
||||||
onUpdateCallback
|
|
||||||
);
|
|
||||||
|
|
||||||
// Simulate media query match
|
|
||||||
capturedCallback?.(true);
|
|
||||||
|
|
||||||
// Should call onUpdate directly with the matches value
|
|
||||||
expect(onUpdateCallback).toHaveBeenCalledWith(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("multiple media queries", () => {
|
|
||||||
it("should setup listeners for multiple screen conditions", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "screen",
|
|
||||||
media_query: "(max-width: 600px)",
|
|
||||||
} as ScreenCondition,
|
|
||||||
{
|
|
||||||
condition: "screen",
|
|
||||||
media_query: "(orientation: portrait)",
|
|
||||||
} as ScreenCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
setupMediaQueryListeners(
|
|
||||||
conditions,
|
|
||||||
hass,
|
|
||||||
(unsub) => listeners.push(unsub),
|
|
||||||
onUpdateCallback
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(listenMediaQuerySpy).toHaveBeenCalledWith(
|
|
||||||
"(max-width: 600px)",
|
|
||||||
expect.any(Function)
|
|
||||||
);
|
|
||||||
expect(listenMediaQuerySpy).toHaveBeenCalledWith(
|
|
||||||
"(orientation: portrait)",
|
|
||||||
expect.any(Function)
|
|
||||||
);
|
|
||||||
expect(listeners).toHaveLength(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call onUpdate when media query changes with mixed conditions", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "screen",
|
|
||||||
media_query: "(max-width: 600px)",
|
|
||||||
} as ScreenCondition,
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
} as TimeCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
let capturedCallback: ((matches: boolean) => void) | undefined;
|
|
||||||
|
|
||||||
listenMediaQuerySpy.mockImplementation((_query, callback) => {
|
|
||||||
capturedCallback = callback;
|
|
||||||
return vi.fn();
|
|
||||||
});
|
|
||||||
|
|
||||||
setupMediaQueryListeners(
|
|
||||||
conditions,
|
|
||||||
hass,
|
|
||||||
(unsub) => listeners.push(unsub),
|
|
||||||
onUpdateCallback
|
|
||||||
);
|
|
||||||
|
|
||||||
// Simulate media query change
|
|
||||||
capturedCallback?.(true);
|
|
||||||
|
|
||||||
// Should call onUpdate (would check all conditions)
|
|
||||||
expect(onUpdateCallback).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("no screen conditions", () => {
|
|
||||||
it("should not setup listeners when no screen conditions exist", () => {
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
} as TimeCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
setupMediaQueryListeners(
|
|
||||||
conditions,
|
|
||||||
hass,
|
|
||||||
(unsub) => listeners.push(unsub),
|
|
||||||
onUpdateCallback
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(listenMediaQuerySpy).not.toHaveBeenCalled();
|
|
||||||
expect(listeners).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle empty conditions array", () => {
|
|
||||||
setupMediaQueryListeners(
|
|
||||||
[],
|
|
||||||
hass,
|
|
||||||
(unsub) => listeners.push(unsub),
|
|
||||||
onUpdateCallback
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(listenMediaQuerySpy).not.toHaveBeenCalled();
|
|
||||||
expect(listeners).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("listener cleanup", () => {
|
|
||||||
it("should register cleanup functions", () => {
|
|
||||||
const unsubFn = vi.fn();
|
|
||||||
|
|
||||||
listenMediaQuerySpy.mockReturnValue(unsubFn);
|
|
||||||
|
|
||||||
const conditions: Condition[] = [
|
|
||||||
{
|
|
||||||
condition: "screen",
|
|
||||||
media_query: "(max-width: 600px)",
|
|
||||||
} as ScreenCondition,
|
|
||||||
];
|
|
||||||
|
|
||||||
setupMediaQueryListeners(
|
|
||||||
conditions,
|
|
||||||
hass,
|
|
||||||
(unsub) => listeners.push(unsub),
|
|
||||||
onUpdateCallback
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(listeners).toHaveLength(1);
|
|
||||||
|
|
||||||
// Call cleanup
|
|
||||||
listeners[0]();
|
|
||||||
|
|
||||||
// Should have called the unsubscribe function
|
|
||||||
expect(unsubFn).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,320 +0,0 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
||||||
import { calculateNextTimeUpdate } from "../../../src/common/condition/time-calculator";
|
|
||||||
import type { HomeAssistant } from "../../../src/types";
|
|
||||||
import {
|
|
||||||
NumberFormat,
|
|
||||||
TimeFormat,
|
|
||||||
FirstWeekday,
|
|
||||||
DateFormat,
|
|
||||||
TimeZone,
|
|
||||||
} from "../../../src/data/translation";
|
|
||||||
|
|
||||||
describe("calculateNextTimeUpdate", () => {
|
|
||||||
let mockHass: HomeAssistant;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockHass = {
|
|
||||||
locale: {
|
|
||||||
language: "en-US",
|
|
||||||
number_format: NumberFormat.language,
|
|
||||||
time_format: TimeFormat.language,
|
|
||||||
date_format: DateFormat.language,
|
|
||||||
time_zone: TimeZone.local,
|
|
||||||
first_weekday: FirstWeekday.language,
|
|
||||||
},
|
|
||||||
config: {
|
|
||||||
time_zone: "America/Los_Angeles",
|
|
||||||
},
|
|
||||||
} as HomeAssistant;
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("after time calculation", () => {
|
|
||||||
it("should calculate time until after time today when it hasn't passed", () => {
|
|
||||||
// Set time to 7:00 AM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 7, 0, 0));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, { after: "08:00" });
|
|
||||||
|
|
||||||
// Should be ~1 hour + 1 minute buffer = 61 minutes
|
|
||||||
expect(result).toBeGreaterThan(60 * 60 * 1000); // > 60 minutes
|
|
||||||
expect(result).toBeLessThan(62 * 60 * 1000); // < 62 minutes
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should calculate time until after time tomorrow when it has passed", () => {
|
|
||||||
// Set time to 9:00 AM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 9, 0, 0));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, { after: "08:00" });
|
|
||||||
|
|
||||||
// Should be ~23 hours + 1 minute buffer
|
|
||||||
expect(result).toBeGreaterThan(23 * 60 * 60 * 1000);
|
|
||||||
expect(result).toBeLessThan(24 * 60 * 60 * 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle after time exactly at current time", () => {
|
|
||||||
// Set time to 8:00 AM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 8, 0, 0));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, { after: "08:00" });
|
|
||||||
|
|
||||||
// Should be scheduled for tomorrow
|
|
||||||
expect(result).toBeGreaterThan(23 * 60 * 60 * 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle after time with seconds", () => {
|
|
||||||
// Set time to 7:59:30 AM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 7, 59, 30));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, { after: "08:00:00" });
|
|
||||||
|
|
||||||
// Should be ~30 seconds + 1 minute buffer
|
|
||||||
expect(result).toBeGreaterThan(30 * 1000);
|
|
||||||
expect(result).toBeLessThan(2 * 60 * 1000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("before time calculation", () => {
|
|
||||||
it("should calculate time until before time today when it hasn't passed", () => {
|
|
||||||
// Set time to 4:00 PM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 16, 0, 0));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, { before: "17:00" });
|
|
||||||
|
|
||||||
// Should be ~1 hour + 1 minute buffer
|
|
||||||
expect(result).toBeGreaterThan(60 * 60 * 1000);
|
|
||||||
expect(result).toBeLessThan(62 * 60 * 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should calculate time until before time tomorrow when it has passed", () => {
|
|
||||||
// Set time to 6:00 PM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 18, 0, 0));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, { before: "17:00" });
|
|
||||||
|
|
||||||
// Should be ~23 hours + 1 minute buffer
|
|
||||||
expect(result).toBeGreaterThan(23 * 60 * 60 * 1000);
|
|
||||||
expect(result).toBeLessThan(24 * 60 * 60 * 1000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("combined after and before", () => {
|
|
||||||
it("should return the soonest boundary when both are in the future", () => {
|
|
||||||
// Set time to 7:00 AM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 7, 0, 0));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, {
|
|
||||||
after: "08:00",
|
|
||||||
before: "17:00",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should return time until 8:00 AM (soonest)
|
|
||||||
expect(result).toBeGreaterThan(60 * 60 * 1000); // > 60 minutes
|
|
||||||
expect(result).toBeLessThan(62 * 60 * 1000); // < 62 minutes
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return the soonest boundary when within the range", () => {
|
|
||||||
// Set time to 10:00 AM (within 08:00-17:00 range)
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 10, 0, 0));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, {
|
|
||||||
after: "08:00",
|
|
||||||
before: "17:00",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should return time until 5:00 PM (next boundary)
|
|
||||||
expect(result).toBeGreaterThan(7 * 60 * 60 * 1000); // > 7 hours
|
|
||||||
expect(result).toBeLessThan(8 * 60 * 60 * 1000); // < 8 hours
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle midnight crossing range", () => {
|
|
||||||
// Set time to 11:00 PM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 23, 0, 0));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, {
|
|
||||||
after: "22:00",
|
|
||||||
before: "06:00",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should return time until 6:00 AM (next boundary)
|
|
||||||
expect(result).toBeGreaterThan(7 * 60 * 60 * 1000); // > 7 hours
|
|
||||||
expect(result).toBeLessThan(8 * 60 * 60 * 1000); // < 8 hours
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("weekday boundaries", () => {
|
|
||||||
it("should schedule for midnight when weekdays are specified (not all 7)", () => {
|
|
||||||
// Set time to Monday 10:00 AM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 10, 0, 0));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, {
|
|
||||||
weekdays: ["mon", "wed", "fri"],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should be scheduled for midnight (Tuesday)
|
|
||||||
expect(result).toBeGreaterThan(14 * 60 * 60 * 1000); // > 14 hours
|
|
||||||
expect(result).toBeLessThan(15 * 60 * 60 * 1000); // < 15 hours
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not schedule midnight when all 7 weekdays specified", () => {
|
|
||||||
// Set time to Monday 10:00 AM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 10, 0, 0));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, {
|
|
||||||
weekdays: ["mon", "tue", "wed", "thu", "fri", "sat", "sun"],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should return undefined (no boundaries)
|
|
||||||
expect(result).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should combine weekday midnight with after time", () => {
|
|
||||||
// Set time to Monday 7:00 AM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 7, 0, 0));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, {
|
|
||||||
after: "08:00",
|
|
||||||
weekdays: ["mon", "wed", "fri"],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should return the soonest (8:00 AM is sooner than midnight)
|
|
||||||
expect(result).toBeGreaterThan(60 * 60 * 1000); // > 60 minutes
|
|
||||||
expect(result).toBeLessThan(62 * 60 * 1000); // < 62 minutes
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should prefer midnight over later time boundary", () => {
|
|
||||||
// Set time to Monday 11:00 PM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 23, 0, 0));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, {
|
|
||||||
after: "08:00",
|
|
||||||
weekdays: ["mon", "wed", "fri"],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should return midnight (sooner than 8:00 AM)
|
|
||||||
expect(result).toBeGreaterThan(60 * 60 * 1000); // > 1 hour
|
|
||||||
expect(result).toBeLessThan(2 * 60 * 60 * 1000); // < 2 hours
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("no boundaries", () => {
|
|
||||||
it("should return undefined when no conditions specified", () => {
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 10, 0, 0));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, {});
|
|
||||||
|
|
||||||
expect(result).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return undefined when only all weekdays specified", () => {
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 10, 0, 0));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, {
|
|
||||||
weekdays: ["mon", "tue", "wed", "thu", "fri", "sat", "sun"],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return undefined when empty weekdays array", () => {
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 10, 0, 0));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, {
|
|
||||||
weekdays: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("buffer addition", () => {
|
|
||||||
it("should add 1 minute buffer to next update time", () => {
|
|
||||||
// Set time to 7:59 AM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 7, 59, 0));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, { after: "08:00" });
|
|
||||||
|
|
||||||
// Should be ~1 minute for the boundary + 1 minute buffer = ~2 minutes
|
|
||||||
expect(result).toBeGreaterThan(60 * 1000); // > 1 minute
|
|
||||||
expect(result).toBeLessThan(3 * 60 * 1000); // < 3 minutes
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("timezone handling", () => {
|
|
||||||
it("should use server timezone when configured", () => {
|
|
||||||
mockHass.locale.time_zone = TimeZone.server;
|
|
||||||
mockHass.config.time_zone = "America/New_York";
|
|
||||||
|
|
||||||
// Set time to 7:00 AM local time
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 7, 0, 0));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, { after: "08:00" });
|
|
||||||
|
|
||||||
// Should calculate based on server timezone
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should use local timezone when configured", () => {
|
|
||||||
mockHass.locale.time_zone = TimeZone.local;
|
|
||||||
|
|
||||||
// Set time to 7:00 AM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 7, 0, 0));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, { after: "08:00" });
|
|
||||||
|
|
||||||
// Should calculate based on local timezone
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("edge cases", () => {
|
|
||||||
it("should handle midnight (00:00) as after time", () => {
|
|
||||||
// Set time to 11:00 PM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 23, 0, 0));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, { after: "00:00" });
|
|
||||||
|
|
||||||
// Should be ~1 hour + 1 minute buffer until midnight
|
|
||||||
expect(result).toBeGreaterThan(60 * 60 * 1000);
|
|
||||||
expect(result).toBeLessThan(62 * 60 * 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle 23:59 as before time", () => {
|
|
||||||
// Set time to 11:00 PM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 23, 0, 0));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, { before: "23:59" });
|
|
||||||
|
|
||||||
// Should be ~59 minutes + 1 minute buffer
|
|
||||||
expect(result).toBeGreaterThan(59 * 60 * 1000);
|
|
||||||
expect(result).toBeLessThan(61 * 60 * 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle very close boundary (seconds away)", () => {
|
|
||||||
// Set time to 7:59:50 AM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 7, 59, 50));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, { after: "08:00:00" });
|
|
||||||
|
|
||||||
// Should be ~10 seconds + 1 minute buffer
|
|
||||||
expect(result).toBeGreaterThan(10 * 1000);
|
|
||||||
expect(result).toBeLessThan(2 * 60 * 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle DST transition correctly", () => {
|
|
||||||
// March 10, 2024 at 1:00 AM PST - before spring forward
|
|
||||||
vi.setSystemTime(new Date(2024, 2, 10, 1, 0, 0));
|
|
||||||
|
|
||||||
const result = calculateNextTimeUpdate(mockHass, { after: "03:00" });
|
|
||||||
|
|
||||||
// Should handle the transition where 2:00 AM doesn't exist
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,307 +0,0 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
||||||
import {
|
|
||||||
checkTimeInRange,
|
|
||||||
isValidTimeString,
|
|
||||||
} from "../../../src/common/datetime/check_time";
|
|
||||||
import type { HomeAssistant } from "../../../src/types";
|
|
||||||
import {
|
|
||||||
NumberFormat,
|
|
||||||
TimeFormat,
|
|
||||||
FirstWeekday,
|
|
||||||
DateFormat,
|
|
||||||
TimeZone,
|
|
||||||
} from "../../../src/data/translation";
|
|
||||||
|
|
||||||
describe("isValidTimeString", () => {
|
|
||||||
it("should accept valid HH:MM format", () => {
|
|
||||||
expect(isValidTimeString("08:00")).toBe(true);
|
|
||||||
expect(isValidTimeString("23:59")).toBe(true);
|
|
||||||
expect(isValidTimeString("00:00")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should accept valid HH:MM:SS format", () => {
|
|
||||||
expect(isValidTimeString("08:00:30")).toBe(true);
|
|
||||||
expect(isValidTimeString("23:59:59")).toBe(true);
|
|
||||||
expect(isValidTimeString("00:00:00")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject invalid formats", () => {
|
|
||||||
expect(isValidTimeString("")).toBe(false);
|
|
||||||
expect(isValidTimeString("8")).toBe(false);
|
|
||||||
expect(isValidTimeString("8:00 AM")).toBe(false);
|
|
||||||
expect(isValidTimeString("08:00:00:00")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject invalid hour values", () => {
|
|
||||||
expect(isValidTimeString("24:00")).toBe(false);
|
|
||||||
expect(isValidTimeString("-01:00")).toBe(false);
|
|
||||||
expect(isValidTimeString("25:00")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject invalid minute values", () => {
|
|
||||||
expect(isValidTimeString("08:60")).toBe(false);
|
|
||||||
expect(isValidTimeString("08:-01")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject invalid second values", () => {
|
|
||||||
expect(isValidTimeString("08:00:60")).toBe(false);
|
|
||||||
expect(isValidTimeString("08:00:-01")).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("checkTimeInRange", () => {
|
|
||||||
let mockHass: HomeAssistant;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockHass = {
|
|
||||||
locale: {
|
|
||||||
language: "en-US",
|
|
||||||
number_format: NumberFormat.language,
|
|
||||||
time_format: TimeFormat.language,
|
|
||||||
date_format: DateFormat.language,
|
|
||||||
time_zone: TimeZone.local,
|
|
||||||
first_weekday: FirstWeekday.language,
|
|
||||||
},
|
|
||||||
config: {
|
|
||||||
time_zone: "America/Los_Angeles",
|
|
||||||
},
|
|
||||||
} as HomeAssistant;
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("time ranges within same day", () => {
|
|
||||||
it("should return true when current time is within range", () => {
|
|
||||||
// Set time to 10:00 AM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 10, 0, 0));
|
|
||||||
|
|
||||||
expect(
|
|
||||||
checkTimeInRange(mockHass, { after: "08:00", before: "17:00" })
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return false when current time is before range", () => {
|
|
||||||
// Set time to 7:00 AM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 7, 0, 0));
|
|
||||||
|
|
||||||
expect(
|
|
||||||
checkTimeInRange(mockHass, { after: "08:00", before: "17:00" })
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return false when current time is after range", () => {
|
|
||||||
// Set time to 6:00 PM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 18, 0, 0));
|
|
||||||
|
|
||||||
expect(
|
|
||||||
checkTimeInRange(mockHass, { after: "08:00", before: "17:00" })
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("time ranges crossing midnight", () => {
|
|
||||||
it("should return true when current time is before midnight", () => {
|
|
||||||
// Set time to 11:00 PM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 23, 0, 0));
|
|
||||||
|
|
||||||
expect(
|
|
||||||
checkTimeInRange(mockHass, { after: "22:00", before: "06:00" })
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return true exactly at the after boundary", () => {
|
|
||||||
// Set time to 10:00 PM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 22, 0, 0));
|
|
||||||
|
|
||||||
expect(
|
|
||||||
checkTimeInRange(mockHass, { after: "22:00", before: "06:00" })
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return true when current time is after midnight", () => {
|
|
||||||
// Set time to 3:00 AM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 3, 0, 0));
|
|
||||||
|
|
||||||
expect(
|
|
||||||
checkTimeInRange(mockHass, { after: "22:00", before: "06:00" })
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return true exactly at the before boundary", () => {
|
|
||||||
// Set time to 6:00 AM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 6, 0, 0));
|
|
||||||
|
|
||||||
expect(
|
|
||||||
checkTimeInRange(mockHass, { after: "22:00", before: "06:00" })
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return false when outside the range", () => {
|
|
||||||
// Set time to 10:00 AM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 10, 0, 0));
|
|
||||||
|
|
||||||
expect(
|
|
||||||
checkTimeInRange(mockHass, { after: "22:00", before: "06:00" })
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("only 'after' condition", () => {
|
|
||||||
it("should return true when after specified time", () => {
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 10, 0, 0));
|
|
||||||
expect(checkTimeInRange(mockHass, { after: "08:00" })).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return false when before specified time", () => {
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 6, 0, 0));
|
|
||||||
expect(checkTimeInRange(mockHass, { after: "08:00" })).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("only 'before' condition", () => {
|
|
||||||
it("should return true when before specified time", () => {
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 10, 0, 0));
|
|
||||||
expect(checkTimeInRange(mockHass, { before: "17:00" })).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return false when after specified time", () => {
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 18, 0, 0));
|
|
||||||
expect(checkTimeInRange(mockHass, { before: "17:00" })).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("weekday filtering", () => {
|
|
||||||
it("should return true on matching weekday", () => {
|
|
||||||
// January 15, 2024 is a Monday
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 10, 0, 0));
|
|
||||||
|
|
||||||
expect(checkTimeInRange(mockHass, { weekdays: ["mon"] })).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return false on non-matching weekday", () => {
|
|
||||||
// January 15, 2024 is a Monday
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 10, 0, 0));
|
|
||||||
|
|
||||||
expect(checkTimeInRange(mockHass, { weekdays: ["tue"] })).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should work with multiple weekdays", () => {
|
|
||||||
// January 15, 2024 is a Monday
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 10, 0, 0));
|
|
||||||
|
|
||||||
expect(
|
|
||||||
checkTimeInRange(mockHass, { weekdays: ["mon", "wed", "fri"] })
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("combined time and weekday conditions", () => {
|
|
||||||
it("should return true when both match", () => {
|
|
||||||
// January 15, 2024 is a Monday at 10:00 AM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 10, 0, 0));
|
|
||||||
|
|
||||||
expect(
|
|
||||||
checkTimeInRange(mockHass, {
|
|
||||||
after: "08:00",
|
|
||||||
before: "17:00",
|
|
||||||
weekdays: ["mon"],
|
|
||||||
})
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return false when time matches but weekday doesn't", () => {
|
|
||||||
// January 15, 2024 is a Monday at 10:00 AM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 10, 0, 0));
|
|
||||||
|
|
||||||
expect(
|
|
||||||
checkTimeInRange(mockHass, {
|
|
||||||
after: "08:00",
|
|
||||||
before: "17:00",
|
|
||||||
weekdays: ["tue"],
|
|
||||||
})
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return false when weekday matches but time doesn't", () => {
|
|
||||||
// January 15, 2024 is a Monday at 6:00 AM
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 6, 0, 0));
|
|
||||||
|
|
||||||
expect(
|
|
||||||
checkTimeInRange(mockHass, {
|
|
||||||
after: "08:00",
|
|
||||||
before: "17:00",
|
|
||||||
weekdays: ["mon"],
|
|
||||||
})
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("no conditions", () => {
|
|
||||||
it("should return true when no conditions specified", () => {
|
|
||||||
vi.setSystemTime(new Date(2024, 0, 15, 10, 0, 0));
|
|
||||||
|
|
||||||
expect(
|
|
||||||
checkTimeInRange(mockHass, { after: "08:00", before: "17:00" })
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("DST transitions", () => {
|
|
||||||
it("should handle spring forward transition (losing an hour)", () => {
|
|
||||||
// March 10, 2024 at 1:30 AM PST - before spring forward
|
|
||||||
// At 2:00 AM, clocks jump to 3:00 AM PDT
|
|
||||||
vi.setSystemTime(new Date(2024, 2, 10, 1, 30, 0));
|
|
||||||
|
|
||||||
// Should be within range that crosses the transition
|
|
||||||
expect(
|
|
||||||
checkTimeInRange(mockHass, { after: "01:00", before: "04:00" })
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle spring forward transition after the jump", () => {
|
|
||||||
// March 10, 2024 at 3:30 AM PDT - after spring forward
|
|
||||||
vi.setSystemTime(new Date(2024, 2, 10, 3, 30, 0));
|
|
||||||
|
|
||||||
// Should still be within range
|
|
||||||
expect(
|
|
||||||
checkTimeInRange(mockHass, { after: "01:00", before: "04:00" })
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle fall back transition (gaining an hour)", () => {
|
|
||||||
// November 3, 2024 at 1:30 AM PDT - before fall back
|
|
||||||
// At 2:00 AM PDT, clocks fall back to 1:00 AM PST
|
|
||||||
vi.setSystemTime(new Date(2024, 10, 3, 1, 30, 0));
|
|
||||||
|
|
||||||
// Should be within range that crosses the transition
|
|
||||||
expect(
|
|
||||||
checkTimeInRange(mockHass, { after: "01:00", before: "03:00" })
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle midnight crossing during DST transition", () => {
|
|
||||||
// March 10, 2024 at 1:00 AM - during spring forward night
|
|
||||||
vi.setSystemTime(new Date(2024, 2, 10, 1, 0, 0));
|
|
||||||
|
|
||||||
// Range that crosses midnight and DST transition
|
|
||||||
expect(
|
|
||||||
checkTimeInRange(mockHass, { after: "22:00", before: "04:00" })
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should correctly compare times on DST transition day", () => {
|
|
||||||
// November 3, 2024 at 10:00 AM - after fall back completed
|
|
||||||
vi.setSystemTime(new Date(2024, 10, 3, 10, 0, 0));
|
|
||||||
|
|
||||||
// Normal business hours should work correctly
|
|
||||||
expect(
|
|
||||||
checkTimeInRange(mockHass, { after: "08:00", before: "17:00" })
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
checkTimeInRange(mockHass, { after: "12:00", before: "17:00" })
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { assert, describe, it } from "vitest";
|
|
||||||
|
|
||||||
import { parseAnimationDuration } from "../../../src/common/util/parse-animation-duration";
|
|
||||||
|
|
||||||
describe("parseAnimationDuration", () => {
|
|
||||||
it("Parses milliseconds with unit", () => {
|
|
||||||
assert.equal(parseAnimationDuration("300ms"), 300);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Parses seconds with unit", () => {
|
|
||||||
assert.equal(parseAnimationDuration("3s"), 3000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Parses decimal seconds", () => {
|
|
||||||
assert.equal(parseAnimationDuration("0.5s"), 500);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Parses decimal milliseconds", () => {
|
|
||||||
assert.equal(parseAnimationDuration("250.5ms"), 250.5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Handles whitespace", () => {
|
|
||||||
assert.equal(parseAnimationDuration(" 300ms "), 300);
|
|
||||||
assert.equal(parseAnimationDuration(" 3s "), 3000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Handles number without unit as milliseconds", () => {
|
|
||||||
assert.equal(parseAnimationDuration("300"), 300);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Returns 0 for invalid input", () => {
|
|
||||||
assert.equal(parseAnimationDuration("invalid"), 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Returns 0 for empty string", () => {
|
|
||||||
assert.equal(parseAnimationDuration(""), 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Returns 0 for negative values", () => {
|
|
||||||
assert.equal(parseAnimationDuration("-300ms"), 0);
|
|
||||||
assert.equal(parseAnimationDuration("-3s"), 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Returns 0 for NaN", () => {
|
|
||||||
assert.equal(parseAnimationDuration("NaN"), 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Returns 0 for Infinity", () => {
|
|
||||||
assert.equal(parseAnimationDuration("Infinity"), 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Handles zero values", () => {
|
|
||||||
assert.equal(parseAnimationDuration("0ms"), 0);
|
|
||||||
assert.equal(parseAnimationDuration("0s"), 0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
|
||||||
import { validateConditionalConfig } from "../../../../src/panels/lovelace/common/validate-condition";
|
|
||||||
import type { TimeCondition } from "../../../../src/panels/lovelace/common/validate-condition";
|
|
||||||
|
|
||||||
describe("validateConditionalConfig - TimeCondition", () => {
|
|
||||||
describe("valid configurations", () => {
|
|
||||||
it("should accept valid after time", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should accept valid before time", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
before: "17:00",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should accept valid after and before times", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
before: "17:00",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should accept time with seconds", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00:30",
|
|
||||||
before: "17:30:45",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should accept only weekdays", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
weekdays: ["mon", "wed", "fri"],
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should accept time and weekdays combined", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
before: "17:00",
|
|
||||||
weekdays: ["mon", "tue", "wed", "thu", "fri"],
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should accept midnight times", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "00:00",
|
|
||||||
before: "23:59",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should accept midnight crossing ranges", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "22:00",
|
|
||||||
before: "06:00",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("invalid time formats", () => {
|
|
||||||
it("should reject invalid hour (> 23)", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "25:00",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject invalid hour (< 0)", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "-01:00",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject invalid minute (> 59)", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "08:60",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject invalid minute (< 0)", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "08:-01",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject invalid second (> 59)", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00:60",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject invalid second (< 0)", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00:-01",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject non-numeric values", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "08:XX",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject 12-hour format", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "8:00 AM",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject single number", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "8",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject too many parts", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00:00:00",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject empty string", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("invalid configurations", () => {
|
|
||||||
it("should reject when after and before are identical", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "09:00",
|
|
||||||
before: "09:00",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject when no conditions specified", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject invalid weekday", () => {
|
|
||||||
const condition = {
|
|
||||||
condition: "time",
|
|
||||||
weekdays: ["monday"], // Should be "mon" not "monday"
|
|
||||||
} as any;
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject empty weekdays array", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
weekdays: [],
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("edge cases", () => {
|
|
||||||
it("should accept single-digit hours with leading zero", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
before: "09:00",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject single-digit hours without leading zero", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "8:00",
|
|
||||||
};
|
|
||||||
// This should be rejected as hours should have 2 digits in HH:MM format
|
|
||||||
// However, parseInt will parse it successfully, so this will pass
|
|
||||||
// This is acceptable for flexibility
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should accept 00:00:00", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "00:00:00",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should accept 23:59:59", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
before: "23:59:59",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject both invalid times even if one is valid", () => {
|
|
||||||
const condition: TimeCondition = {
|
|
||||||
condition: "time",
|
|
||||||
after: "08:00",
|
|
||||||
before: "25:00",
|
|
||||||
};
|
|
||||||
expect(validateConditionalConfig([condition])).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
20
yarn.lock
20
yarn.lock
@@ -1940,9 +1940,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@home-assistant/webawesome@npm:3.0.0":
|
"@home-assistant/webawesome@npm:3.0.0-beta.6.ha.7":
|
||||||
version: 3.0.0
|
version: 3.0.0-beta.6.ha.7
|
||||||
resolution: "@home-assistant/webawesome@npm:3.0.0"
|
resolution: "@home-assistant/webawesome@npm:3.0.0-beta.6.ha.7"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ctrl/tinycolor": "npm:4.1.0"
|
"@ctrl/tinycolor": "npm:4.1.0"
|
||||||
"@floating-ui/dom": "npm:^1.6.13"
|
"@floating-ui/dom": "npm:^1.6.13"
|
||||||
@@ -1953,7 +1953,7 @@ __metadata:
|
|||||||
lit: "npm:^3.2.1"
|
lit: "npm:^3.2.1"
|
||||||
nanoid: "npm:^5.1.5"
|
nanoid: "npm:^5.1.5"
|
||||||
qr-creator: "npm:^1.0.0"
|
qr-creator: "npm:^1.0.0"
|
||||||
checksum: 10/03400894cfee8548fd5b1f5c56d31d253830e704b18ba69d36ce6b761d8b1bef2fb52cffba8d9b033033bb582f2f51a2d6444d82622f66d70150e2104fcb49e2
|
checksum: 10/c20e5b60920a3cd5bbabb38e73d0a446c54074dcbb843272404b15b6a7e584b8a328393c1e845a2a400588fe15bdcd28d2c18aa2ce44b806f72a3b9343a3310f
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -9226,7 +9226,7 @@ __metadata:
|
|||||||
"@fullcalendar/list": "npm:6.1.19"
|
"@fullcalendar/list": "npm:6.1.19"
|
||||||
"@fullcalendar/luxon3": "npm:6.1.19"
|
"@fullcalendar/luxon3": "npm:6.1.19"
|
||||||
"@fullcalendar/timegrid": "npm:6.1.19"
|
"@fullcalendar/timegrid": "npm:6.1.19"
|
||||||
"@home-assistant/webawesome": "npm:3.0.0"
|
"@home-assistant/webawesome": "npm:3.0.0-beta.6.ha.7"
|
||||||
"@lezer/highlight": "npm:1.2.3"
|
"@lezer/highlight": "npm:1.2.3"
|
||||||
"@lit-labs/motion": "npm:1.0.9"
|
"@lit-labs/motion": "npm:1.0.9"
|
||||||
"@lit-labs/observers": "npm:2.0.6"
|
"@lit-labs/observers": "npm:2.0.6"
|
||||||
@@ -9351,7 +9351,7 @@ __metadata:
|
|||||||
lodash.template: "npm:4.5.0"
|
lodash.template: "npm:4.5.0"
|
||||||
luxon: "npm:3.7.2"
|
luxon: "npm:3.7.2"
|
||||||
map-stream: "npm:0.0.7"
|
map-stream: "npm:0.0.7"
|
||||||
marked: "npm:17.0.0"
|
marked: "npm:16.4.2"
|
||||||
memoize-one: "npm:6.0.0"
|
memoize-one: "npm:6.0.0"
|
||||||
node-vibrant: "npm:4.0.3"
|
node-vibrant: "npm:4.0.3"
|
||||||
object-hash: "npm:3.0.0"
|
object-hash: "npm:3.0.0"
|
||||||
@@ -10984,12 +10984,12 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"marked@npm:17.0.0":
|
"marked@npm:16.4.2":
|
||||||
version: 17.0.0
|
version: 16.4.2
|
||||||
resolution: "marked@npm:17.0.0"
|
resolution: "marked@npm:16.4.2"
|
||||||
bin:
|
bin:
|
||||||
marked: bin/marked.js
|
marked: bin/marked.js
|
||||||
checksum: 10/5544d27547851986c4e994a3f5739ea30bfe2616a9e1d5d5c8ced0fd561b5e971b3c7ee62b4fea1ea530e9886b89102d5c3b3bf962756494ced021f1accd6854
|
checksum: 10/6e40e40661dce97e271198daa2054fc31e6445892a735e416c248fba046bdfa4573cafa08dc254529f105e7178a34485eb7f82573979cfb377a4530f66e79187
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user