Compare commits

..

27 Commits

Author SHA1 Message Date
Aidan Timson
5d325b5627 Remove seconds 2025-11-05 12:32:14 +00:00
Aidan Timson
ef0368f9c3 Update src/common/datetime/check_time.ts
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-11-05 07:57:47 +00:00
Aidan Timson
8fc305ec6b Add tests for DST transitions 2025-11-04 11:32:45 +00:00
Aidan Timson
fd657fbf68 Add missing import 2025-11-04 11:32:45 +00:00
Aidan Timson
885f4564a9 Docstring 2025-11-04 11:32:45 +00:00
Aidan Timson
d27c2e3a04 Cleanup 2025-11-04 11:32:45 +00:00
Aidan Timson
f2655ec226 Use satisfies for typing 2025-11-04 11:32:45 +00:00
Aidan Timson
1c3043ed37 Validate time range 2025-11-04 11:32:45 +00:00
Aidan Timson
86bfb6a482 Remove mutability 2025-11-04 11:32:45 +00:00
Aidan Timson
bd3a1ea281 Add tests 2025-11-04 11:32:45 +00:00
Aidan Timson
90757b6f97 Check format 2025-11-04 11:32:45 +00:00
Aidan Timson
6a2bbb02a2 Add default 2025-11-04 11:32:45 +00:00
Aidan Timson
698197dccd Improve validation 2025-11-04 11:32:45 +00:00
Aidan Timson
665ff88d58 Add translations 2025-11-04 11:32:45 +00:00
Aidan Timson
bb8dd69e7b Setup 2025-11-04 11:32:45 +00:00
Aidan Timson
819182977c Add condition function 2025-11-04 11:32:45 +00:00
Aidan Timson
2d838e22d2 Make weekday types 2025-11-04 11:32:45 +00:00
Aidan Timson
036cbe0a1a Rename to time 2025-11-04 11:32:45 +00:00
Aidan Timson
6fc039340e Skeleton 2025-11-04 11:32:45 +00:00
Tobias Bieniek
e1b0a3e737 Hide media players summary when no entities exist (#27642) 2025-11-04 11:44:12 +01:00
Paul Bottein
d59d436080 Display entities without area in summary dashboard (#27777)
* Add support for no area, no floor and no device in entity filter

* Display entities without area in summary dashboard
2025-11-04 12:10:18 +02:00
Wendelin
0fe0bf12f2 Fix-labels-yaml-helper (#27776)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-11-04 09:01:36 +00:00
Paul Bottein
f5a3877f47 Fix tooltip hide delay (#27786) 2025-11-04 09:45:26 +01:00
renovate[bot]
31d04f5338 Update dependency globals to v16.5.0 (#27785)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 08:49:20 +02:00
karwosts
4f7d223aa7 Fix sankey with external statistics devices (#27784) 2025-11-04 08:22:36 +02:00
renovate[bot]
484c60073d Update dependency @octokit/rest to v22.0.1 (#27779)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 06:33:20 +01:00
renovate[bot]
0e1ab1a60c Update dependency hls.js to v1.6.14 (#27780)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 06:33:05 +01:00
41 changed files with 1061 additions and 513 deletions

View File

@@ -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.

View File

@@ -1,133 +0,0 @@
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 "../../../../src/components/ha-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-dropdown-item";
import "@home-assistant/webawesome/dist/components/icon/icon";
import "@home-assistant/webawesome/dist/components/button/button";
import "@home-assistant/webawesome/dist/components/dropdown/dropdown";
import "../../../../src/components/ha-dropdown";
import "@home-assistant/webawesome/dist/components/popup/popup";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
import "../../../../src/components/ha-icon-button";
@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;
}
}

View File

@@ -111,7 +111,7 @@
"fuse.js": "7.1.0",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "6.0.2",
"hls.js": "1.6.13",
"hls.js": "1.6.14",
"home-assistant-js-websocket": "9.5.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "10.7.18",
@@ -156,7 +156,7 @@
"@lokalise/node-api": "15.3.1",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.0",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.3.7",
"@rspack/core": "1.6.0",
"@rspack/dev-server": "1.1.4",
@@ -231,7 +231,7 @@
"clean-css": "5.3.3",
"@lit/reactive-element": "2.1.1",
"@fullcalendar/daygrid": "6.1.19",
"globals": "16.4.0",
"globals": "16.5.0",
"tslib": "2.8.1",
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
},

View File

@@ -0,0 +1,110 @@
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, type WeekdayShort } from "./weekday";
/**
* Parse a time string (HH:MM or HH:MM:SS) and set it on today's date in the given timezone
* @param timeString The time string to parse
* @param timezone The timezone to use
* @returns The Date object
*/
const parseTimeString = (timeString: string, timezone: string): Date => {
const parts = timeString.split(":");
if (parts.length < 2 || parts.length > 3) {
throw new Error(
`Invalid time format: ${timeString}. Expected HH:MM or HH:MM:SS`
);
}
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)) {
throw new Error(`Invalid time values in: ${timeString}`);
}
// Add range validation
if (hours < 0 || hours > 23) {
throw new Error(`Invalid hours in: ${timeString}. Must be 0-23`);
}
if (minutes < 0 || minutes > 59) {
throw new Error(`Invalid minutes in: ${timeString}. Must be 0-59`);
}
if (seconds < 0 || seconds > 59) {
throw new Error(`Invalid seconds in: ${timeString}. Must be 0-59`);
}
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 after Optional time string (HH:MM) for after condition
* @param before Optional time string (HH:MM) for before condition
* @param weekdays Optional array of weekdays to match
* @returns true if current time matches the condition
*/
export const checkTimeInRange = (
hass: HomeAssistant,
after?: string,
before?: string,
weekdays?: WeekdayShort[]
): 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 isAfter(now, afterDate) || isBefore(now, beforeDate);
}
return isWithinInterval(now, { start: afterDate, end: beforeDate });
}
if (afterDate) {
return !isBefore(now, afterDate);
}
if (beforeDate) {
return !isAfter(now, beforeDate);
}
return true;
};

View File

@@ -1,18 +1,7 @@
import { getWeekStartByLocale } from "weekstart";
import type { FrontendLocaleData } from "../../data/translation";
import { FirstWeekday } from "../../data/translation";
export const weekdays = [
"sunday",
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
] as const;
type WeekdayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6;
import { WEEKDAYS_LONG, type WeekdayIndex } from "./weekday";
export const firstWeekdayIndex = (locale: FrontendLocaleData): WeekdayIndex => {
if (locale.first_weekday === FirstWeekday.language) {
@@ -23,12 +12,12 @@ export const firstWeekdayIndex = (locale: FrontendLocaleData): WeekdayIndex => {
}
return (getWeekStartByLocale(locale.language) % 7) as WeekdayIndex;
}
return weekdays.includes(locale.first_weekday)
? (weekdays.indexOf(locale.first_weekday) as WeekdayIndex)
return WEEKDAYS_LONG.includes(locale.first_weekday)
? (WEEKDAYS_LONG.indexOf(locale.first_weekday) as WeekdayIndex)
: 1;
};
export const firstWeekday = (locale: FrontendLocaleData) => {
const index = firstWeekdayIndex(locale);
return weekdays[index];
return WEEKDAYS_LONG[index];
};

View File

@@ -0,0 +1,59 @@
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>;

View File

@@ -9,9 +9,9 @@ type EntityCategory = "none" | "config" | "diagnostic";
export interface EntityFilter {
domain?: string | string[];
device_class?: string | string[];
device?: string | string[];
area?: string | string[];
floor?: string | string[];
device?: string | null | (string | null)[];
area?: string | null | (string | null)[];
floor?: string | null | (string | null)[];
label?: string | string[];
entity_category?: EntityCategory | EntityCategory[];
hidden_platform?: string | string[];
@@ -19,6 +19,18 @@ export interface EntityFilter {
export type EntityFilterFunc = (entityId: string) => boolean;
const normalizeFilterArray = <T>(
value: T | null | T[] | (T | null)[] | undefined
): Set<T | null> | undefined => {
if (value === undefined) {
return undefined;
}
if (value === null) {
return new Set([null]);
}
return new Set(ensureArray(value));
};
export const generateEntityFilter = (
hass: HomeAssistant,
filter: EntityFilter
@@ -29,11 +41,9 @@ export const generateEntityFilter = (
const deviceClasses = filter.device_class
? new Set(ensureArray(filter.device_class))
: undefined;
const floors = filter.floor ? new Set(ensureArray(filter.floor)) : undefined;
const areas = filter.area ? new Set(ensureArray(filter.area)) : undefined;
const devices = filter.device
? new Set(ensureArray(filter.device))
: undefined;
const floors = normalizeFilterArray(filter.floor);
const areas = normalizeFilterArray(filter.area);
const devices = normalizeFilterArray(filter.device);
const entityCategories = filter.entity_category
? new Set(ensureArray(filter.entity_category))
: undefined;
@@ -73,23 +83,20 @@ export const generateEntityFilter = (
}
if (floors) {
if (!floor || !floors.has(floor.floor_id)) {
const floorId = floor?.floor_id ?? null;
if (!floors.has(floorId)) {
return false;
}
}
if (areas) {
if (!area) {
return false;
}
if (!areas.has(area.area_id)) {
const areaId = area?.area_id ?? null;
if (!areas.has(areaId)) {
return false;
}
}
if (devices) {
if (!device) {
return false;
}
if (!devices.has(device.id)) {
const deviceId = device?.id ?? null;
if (!devices.has(deviceId)) {
return false;
}
}

View File

@@ -1,33 +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: 40px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-dropdown-item": HaDropdownItem;
}
}

View File

@@ -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 {
--wa-color-surface-border: var(--ha-color-border-normal);
--wa-color-surface-raised: var(
--card-background-color,
var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
);
}
#menu {
--wa-shadow-m: 0px 4px 8px 0px var(--ha-color-shadow);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-dropdown": HaDropdown;
}
}

View File

@@ -9,7 +9,7 @@ export class HaTooltip extends Tooltip {
@property({ attribute: "show-delay", type: Number }) showDelay = 150;
/** The amount of time to wait before hiding the tooltip when the user mouses out.. */
@property({ attribute: "hide-delay", type: Number }) hideDelay = 400;
@property({ attribute: "hide-delay", type: Number }) hideDelay = 150;
static get styles(): CSSResultGroup {
return [

View File

@@ -12,6 +12,7 @@ import { CONDITION_BUILDING_BLOCKS } from "./condition";
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
import type { Action, Field, MODES } 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_MAX = 10;
@@ -257,13 +258,11 @@ export interface ZoneCondition extends BaseCondition {
zone: string;
}
type Weekday = "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat";
export interface TimeCondition extends BaseCondition {
condition: "time";
after?: string;
before?: string;
weekday?: Weekday | Weekday[];
weekday?: WeekdayShort | WeekdayShort[];
}
export interface TemplateCondition extends BaseCondition {

View File

@@ -115,6 +115,24 @@ const processAreasForClimate = (
return cards;
};
const processUnassignedEntities = (
hass: HomeAssistant,
entities: string[]
): LovelaceCardConfig[] => {
const unassignedFilter = generateEntityFilter(hass, {
area: null,
});
const unassignedEntities = entities.filter(unassignedFilter);
const areaCards: LovelaceCardConfig[] = [];
const computeTileCard = computeAreaTileCardConfig(hass, "", true);
for (const entityId of unassignedEntities) {
areaCards.push(computeTileCard(entityId));
}
return areaCards;
};
@customElement("climate-view-strategy")
export class ClimateViewStrategy extends ReactiveElement {
static async generate(
@@ -190,10 +208,33 @@ export class ClimateViewStrategy extends ReactiveElement {
}
}
// Process unassigned entities
const unassignedCards = processUnassignedEntities(hass, entities);
if (unassignedCards.length > 0) {
const section: LovelaceSectionRawConfig = {
type: "grid",
column_span: 2,
cards: [
{
type: "heading",
heading:
sections.length > 0
? hass.localize(
"ui.panel.lovelace.strategy.climate.other_devices"
)
: hass.localize("ui.panel.lovelace.strategy.climate.devices"),
},
...unassignedCards,
],
};
sections.push(section);
}
return {
type: "sections",
max_columns: 2,
sections: sections || [],
sections: sections,
};
}
}

View File

@@ -4,6 +4,7 @@ import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-button";
import type { ExtEntityRegistryEntry } from "../../../../../data/entity_registry";
import { removeEntityRegistryEntry } from "../../../../../data/entity_registry";
import { HELPERS_CRUD } from "../../../../../data/helpers_crud";
@@ -22,7 +23,6 @@ import "../../../helpers/forms/ha-schedule-form";
import "../../../helpers/forms/ha-timer-form";
import "../../../voice-assistants/entity-voice-settings";
import "../../entity-registry-settings-editor";
import "../../../../../components/ha-button";
import type { EntityRegistrySettingsEditor } from "../../entity-registry-settings-editor";
@customElement("entity-settings-helper-tab")
@@ -72,22 +72,28 @@ export class EntitySettingsHelperTab extends LitElement {
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${this._item === null
? html`<ha-alert alert-type="info"
>${this.hass.localize(
"ui.dialogs.helper_settings.yaml_not_editable"
)}</ha-alert
>`
: nothing}
${!this._componentLoaded
? this.hass.localize(
"ui.dialogs.helper_settings.platform_not_loaded",
{ platform: this.entry.platform }
)
: this._item === null
? this.hass.localize("ui.dialogs.helper_settings.yaml_not_editable")
: html`
<span @value-changed=${this._valueChanged}>
${dynamicElement(`ha-${this.entry.platform}-form`, {
hass: this.hass,
item: this._item,
entry: this.entry,
})}
</span>
`}
: html`
<span @value-changed=${this._valueChanged}>
${dynamicElement(`ha-${this.entry.platform}-form`, {
hass: this.hass,
item: this._item,
entry: this.entry,
disabled: this._item === null,
})}
</span>
`}
<entity-registry-settings-editor
.hass=${this.hass}
.entry=${this.entry}
@@ -122,6 +128,9 @@ export class EntitySettingsHelperTab extends LitElement {
}
private _valueChanged(ev: CustomEvent): void {
if (this._item === null) {
return;
}
this._error = undefined;
this._item = ev.detail.value;
}
@@ -195,6 +204,10 @@ export class EntitySettingsHelperTab extends LitElement {
display: block;
padding: 0 !important;
}
ha-alert {
display: block;
margin-bottom: var(--ha-space-4);
}
.form {
padding: 20px 24px;
}

View File

@@ -784,7 +784,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
<ha-labels-picker
.hass=${this.hass}
.value=${this._labels}
.disabled=${this.disabled}
.disabled=${!!this.disabled}
@value-changed=${this._labelsChanged}
></ha-labels-picker>
${this._cameraPrefs

View File

@@ -17,6 +17,8 @@ class HaCounterForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: Partial<Counter>;
@state() private _name!: string;
@@ -82,6 +84,7 @@ class HaCounterForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -91,6 +94,7 @@ class HaCounterForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
<ha-textfield
.value=${this._minimum}
@@ -100,6 +104,7 @@ class HaCounterForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.minimum"
)}
.disabled=${this.disabled}
></ha-textfield>
<ha-textfield
.value=${this._maximum}
@@ -109,6 +114,7 @@ class HaCounterForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.maximum"
)}
.disabled=${this.disabled}
></ha-textfield>
<ha-textfield
.value=${this._initial}
@@ -118,6 +124,7 @@ class HaCounterForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.initial"
)}
.disabled=${this.disabled}
></ha-textfield>
<ha-expansion-panel
header=${this.hass.localize(
@@ -133,12 +140,14 @@ class HaCounterForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.step"
)}
.disabled=${this.disabled}
></ha-textfield>
<div class="row">
<ha-switch
.checked=${this._restore}
.configValue=${"restore"}
@change=${this._valueChanged}
.disabled=${this.disabled}
>
</ha-switch>
<div>

View File

@@ -14,6 +14,8 @@ class HaInputBooleanForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: InputBoolean;
@state() private _name!: string;
@@ -59,6 +61,7 @@ class HaInputBooleanForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -68,6 +71,7 @@ class HaInputBooleanForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
</div>
`;

View File

@@ -14,6 +14,8 @@ class HaInputButtonForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
@state() private _name!: string;
@state() private _icon!: string;
@@ -59,6 +61,7 @@ class HaInputButtonForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -68,6 +71,7 @@ class HaInputButtonForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
</div>
`;

View File

@@ -17,6 +17,8 @@ class HaInputDateTimeForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: InputDateTime;
@state() private _name!: string;
@@ -73,6 +75,7 @@ class HaInputDateTimeForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -82,6 +85,7 @@ class HaInputDateTimeForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
<br />
${this.hass.localize("ui.dialogs.helper_settings.input_datetime.mode")}:
@@ -97,6 +101,7 @@ class HaInputDateTimeForm extends LitElement {
value="date"
.checked=${this._mode === "date"}
@change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio>
</ha-formfield>
<ha-formfield
@@ -109,6 +114,7 @@ class HaInputDateTimeForm extends LitElement {
value="time"
.checked=${this._mode === "time"}
@change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio>
</ha-formfield>
<ha-formfield
@@ -121,6 +127,7 @@ class HaInputDateTimeForm extends LitElement {
value="datetime"
.checked=${this._mode === "datetime"}
@change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio>
</ha-formfield>
</div>

View File

@@ -18,6 +18,8 @@ class HaInputNumberForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: Partial<InputNumber>;
@state() private _name!: string;
@@ -89,6 +91,7 @@ class HaInputNumberForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -98,6 +101,7 @@ class HaInputNumberForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
<ha-textfield
.value=${this._min}
@@ -108,6 +112,7 @@ class HaInputNumberForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_number.min"
)}
.disabled=${this.disabled}
></ha-textfield>
<ha-textfield
.value=${this._max}
@@ -118,6 +123,7 @@ class HaInputNumberForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_number.max"
)}
.disabled=${this.disabled}
></ha-textfield>
<ha-expansion-panel
header=${this.hass.localize(
@@ -139,6 +145,7 @@ class HaInputNumberForm extends LitElement {
value="slider"
.checked=${this._mode === "slider"}
@change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio>
</ha-formfield>
<ha-formfield
@@ -151,6 +158,7 @@ class HaInputNumberForm extends LitElement {
value="box"
.checked=${this._mode === "box"}
@change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio>
</ha-formfield>
</div>
@@ -163,6 +171,7 @@ class HaInputNumberForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_number.step"
)}
.disabled=${this.disabled}
></ha-textfield>
<ha-textfield
@@ -172,6 +181,7 @@ class HaInputNumberForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_number.unit_of_measurement"
)}
.disabled=${this.disabled}
></ha-textfield>
</ha-expansion-panel>
</div>

View File

@@ -23,6 +23,8 @@ class HaInputSelectForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: InputSelect;
@state() private _name!: string;
@@ -86,6 +88,7 @@ class HaInputSelectForm extends LitElement {
)}
.configValue=${"name"}
@input=${this._valueChanged}
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -95,13 +98,18 @@ class HaInputSelectForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
<div class="header">
${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.options"
)}:
</div>
<ha-sortable @item-moved=${this._optionMoved} handle-selector=".handle">
<ha-sortable
@item-moved=${this._optionMoved}
handle-selector=".handle"
.disabled=${this.disabled}
>
<ha-list class="options">
${this._options.length
? repeat(
@@ -124,6 +132,7 @@ class HaInputSelectForm extends LitElement {
"ui.dialogs.helper_settings.input_select.remove_option"
)}
@click=${this._removeOption}
.disabled=${this.disabled}
.path=${mdiDelete}
></ha-icon-button>
</ha-list-item>
@@ -146,8 +155,13 @@ class HaInputSelectForm extends LitElement {
"ui.dialogs.helper_settings.input_select.add_option"
)}
@keydown=${this._handleKeyAdd}
.disabled=${this.disabled}
></ha-textfield>
<ha-button size="small" appearance="plain" @click=${this._addOption}
<ha-button
size="small"
appearance="plain"
@click=${this._addOption}
.disabled=${this.disabled}
>${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.add"
)}</ha-button

View File

@@ -19,6 +19,8 @@ class HaInputTextForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: InputText;
@state() private _name!: string;
@@ -79,6 +81,7 @@ class HaInputTextForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -88,6 +91,7 @@ class HaInputTextForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
<ha-expansion-panel
header=${this.hass.localize(
@@ -105,6 +109,7 @@ class HaInputTextForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_text.min"
)}
.disabled=${this.disabled}
></ha-textfield>
<ha-textfield
.value=${this._max}
@@ -129,6 +134,7 @@ class HaInputTextForm extends LitElement {
value="text"
.checked=${this._mode === "text"}
@change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio>
</ha-formfield>
<ha-formfield
@@ -141,6 +147,7 @@ class HaInputTextForm extends LitElement {
value="password"
.checked=${this._mode === "password"}
@change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio>
</ha-formfield>
</div>
@@ -154,6 +161,7 @@ class HaInputTextForm extends LitElement {
.helper=${this.hass!.localize(
"ui.dialogs.helper_settings.input_text.pattern_helper"
)}
.disabled=${this.disabled}
></ha-textfield>
</ha-expansion-panel>
</div>

View File

@@ -17,9 +17,9 @@ import "../../../../components/ha-textfield";
import type { Schedule, ScheduleDay } from "../../../../data/schedule";
import { weekdays } from "../../../../data/schedule";
import { TimeZone } from "../../../../data/translation";
import { showScheduleBlockInfoDialog } from "./show-dialog-schedule-block-info";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { showScheduleBlockInfoDialog } from "./show-dialog-schedule-block-info";
const defaultFullCalendarConfig: CalendarOptions = {
plugins: [timeGridPlugin, interactionPlugin],
@@ -43,6 +43,8 @@ class HaScheduleForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
@state() private _name!: string;
@state() private _icon!: string;
@@ -132,6 +134,7 @@ class HaScheduleForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -141,8 +144,9 @@ class HaScheduleForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
<div id="calendar"></div>
${!this.disabled ? html`<div id="calendar"></div>` : nothing}
</div>
`;
}
@@ -175,7 +179,9 @@ class HaScheduleForm extends LitElement {
}
protected firstUpdated(): void {
this._setupCalendar();
if (!this.disabled) {
this._setupCalendar();
}
}
private _setupCalendar(): void {

View File

@@ -1,18 +1,18 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { createDurationData } from "../../../../common/datetime/create_duration_data";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-checkbox";
import "../../../../components/ha-duration-input";
import type { HaDurationData } from "../../../../components/ha-duration-input";
import "../../../../components/ha-formfield";
import "../../../../components/ha-icon-picker";
import "../../../../components/ha-duration-input";
import "../../../../components/ha-textfield";
import type { ForDict } from "../../../../data/automation";
import type { DurationDict, Timer } from "../../../../data/timer";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { createDurationData } from "../../../../common/datetime/create_duration_data";
import type { HaDurationData } from "../../../../components/ha-duration-input";
import type { ForDict } from "../../../../data/automation";
@customElement("ha-timer-form")
class HaTimerForm extends LitElement {
@@ -20,6 +20,8 @@ class HaTimerForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: Timer;
@state() private _name!: string;
@@ -77,6 +79,7 @@ class HaTimerForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -86,11 +89,13 @@ class HaTimerForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
<ha-duration-input
.configValue=${"duration"}
.data=${this._duration_data}
@value-changed=${this._valueChanged}
.disabled=${this.disabled}
></ha-duration-input>
<ha-formfield
.label=${this.hass.localize(
@@ -101,6 +106,7 @@ class HaTimerForm extends LitElement {
.configValue=${"restore"}
.checked=${this._restore}
@click=${this._toggleRestore}
.disabled=${this.disabled}
>
</ha-checkbox>
</ha-formfield>
@@ -130,6 +136,9 @@ class HaTimerForm extends LitElement {
}
private _toggleRestore() {
if (this.disabled) {
return;
}
this._restore = !this._restore;
fireEvent(this, "value-changed", {
value: { ...this._item, restore: this._restore },

View File

@@ -61,6 +61,24 @@ const processAreasForLight = (
return cards;
};
const processUnassignedLights = (
hass: HomeAssistant,
entities: string[]
): LovelaceCardConfig[] => {
const unassignedFilter = generateEntityFilter(hass, {
area: null,
});
const unassignedLights = entities.filter(unassignedFilter);
const areaCards: LovelaceCardConfig[] = [];
const computeTileCard = computeAreaTileCardConfig(hass, "", false);
for (const entityId of unassignedLights) {
areaCards.push(computeTileCard(entityId));
}
return areaCards;
};
@customElement("light-view-strategy")
export class LightViewStrategy extends ReactiveElement {
static async generate(
@@ -136,10 +154,30 @@ export class LightViewStrategy extends ReactiveElement {
}
}
// Process unassigned lights
const unassignedCards = processUnassignedLights(hass, entities);
if (unassignedCards.length > 0) {
const section: LovelaceSectionRawConfig = {
type: "grid",
column_span: 2,
cards: [
{
type: "heading",
heading:
sections.length > 0
? hass.localize("ui.panel.lovelace.strategy.light.other_lights")
: hass.localize("ui.panel.lovelace.strategy.light.lights"),
},
...unassignedCards,
],
};
sections.push(section);
}
return {
type: "sections",
max_columns: 2,
sections: sections || [],
sections: sections,
};
}
}

View File

@@ -419,13 +419,15 @@ class HuiEnergySankeyCard
};
deviceNodes.forEach((deviceNode) => {
const entity = this.hass.states[deviceNode.id];
const { area, floor } = getEntityContext(
entity,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const { area, floor } = entity
? getEntityContext(
entity,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
)
: { area: null, floor: null };
if (area) {
if (area.area_id in areas) {
areas[area.area_id].value += deviceNode.value;

View File

@@ -87,11 +87,6 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
const allEntities = Object.keys(this.hass!.states);
const areas = Object.values(this.hass.areas);
const areasFilter = generateEntityFilter(this.hass, {
area: areas.map((area) => area.area_id),
});
const entitiesInsideArea = allEntities.filter(areasFilter);
switch (this._config.summary) {
case "light": {
@@ -100,7 +95,7 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
generateEntityFilter(this.hass!, filter)
);
const lightEntities = findEntities(entitiesInsideArea, lightsFilters);
const lightEntities = findEntities(allEntities, lightsFilters);
const onLights = lightEntities.filter((entityId) => {
const s = this.hass!.states[entityId]?.state;
@@ -153,7 +148,7 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
generateEntityFilter(this.hass!, filter)
);
const safetyEntities = findEntities(entitiesInsideArea, safetyFilters);
const safetyEntities = findEntities(allEntities, safetyFilters);
const locks = safetyEntities.filter((entityId) => {
const domain = computeDomain(entityId);
@@ -204,7 +199,7 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
);
const mediaPlayerEntities = findEntities(
entitiesInsideArea,
allEntities,
mediaPlayerFilters
);

View File

@@ -1,6 +1,7 @@
import {
mdiAccount,
mdiAmpersand,
mdiCalendarClock,
mdiGateOr,
mdiMapMarker,
mdiNotEqualVariant,
@@ -15,6 +16,7 @@ export const ICON_CONDITION: Record<Condition["condition"], string> = {
numeric_state: mdiNumeric,
state: mdiStateMachine,
screen: mdiResponsive,
time: mdiCalendarClock,
user: mdiAccount,
and: mdiAmpersand,
not: mdiNotEqualVariant,

View File

@@ -1,4 +1,9 @@
import { ensureArray } from "../../../common/array/ensure-array";
import { checkTimeInRange } from "../../../common/datetime/check_time";
import {
WEEKDAYS_SHORT,
type WeekdayShort,
} from "../../../common/datetime/weekday";
import type { MediaQueriesListener } from "../../../common/dom/media_query";
import { listenMediaQuery } from "../../../common/dom/media_query";
@@ -12,6 +17,7 @@ export type Condition =
| NumericStateCondition
| StateCondition
| ScreenCondition
| TimeCondition
| UserCondition
| OrCondition
| AndCondition
@@ -52,6 +58,13 @@ export interface ScreenCondition extends BaseCondition {
media_query?: string;
}
export interface TimeCondition extends BaseCondition {
condition: "time";
after?: string;
before?: string;
weekdays?: WeekdayShort[];
}
export interface UserCondition extends BaseCondition {
condition: "user";
users?: string[];
@@ -152,6 +165,15 @@ function checkScreenCondition(condition: ScreenCondition, _: HomeAssistant) {
: false;
}
function checkTimeCondition(condition: TimeCondition, hass: HomeAssistant) {
return checkTimeInRange(
hass,
condition.after,
condition.before,
condition.weekdays
);
}
function checkLocationCondition(
condition: LocationCondition,
hass: HomeAssistant
@@ -197,6 +219,8 @@ export function checkConditionsMet(
return conditions.every((c) => {
if ("condition" in c) {
switch (c.condition) {
case "time":
return checkTimeCondition(c, hass);
case "screen":
return checkScreenCondition(c, hass);
case "user":
@@ -273,6 +297,17 @@ function validateScreenCondition(condition: ScreenCondition) {
return condition.media_query != null;
}
function validateTimeCondition(condition: TimeCondition) {
const hasTime = condition.after != null || condition.before != null;
const hasWeekdays =
condition.weekdays != null && condition.weekdays.length > 0;
const weekdaysValid =
!hasWeekdays ||
condition.weekdays!.every((w: WeekdayShort) => WEEKDAYS_SHORT.includes(w));
return (hasTime || hasWeekdays) && weekdaysValid;
}
function validateUserCondition(condition: UserCondition) {
return condition.users != null;
}
@@ -312,6 +347,8 @@ export function validateConditionalConfig(
switch (c.condition) {
case "screen":
return validateScreenCondition(c);
case "time":
return validateTimeCondition(c);
case "user":
return validateUserCondition(c);
case "location":

View File

@@ -25,6 +25,7 @@ import "./types/ha-card-condition-numeric_state";
import "./types/ha-card-condition-or";
import "./types/ha-card-condition-screen";
import "./types/ha-card-condition-state";
import "./types/ha-card-condition-time";
import "./types/ha-card-condition-user";
import { storage } from "../../../../common/decorators/storage";
@@ -33,6 +34,7 @@ const UI_CONDITION = [
"numeric_state",
"state",
"screen",
"time",
"user",
"and",
"not",

View File

@@ -0,0 +1,102 @@
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;
}
}

View File

@@ -144,67 +144,80 @@ export class HomeMainViewStrategy extends ReactiveElement {
column_span: maxColumns,
} as LovelaceStrategySectionConfig;
// Check if there are any media player entities
const mediaPlayerFilter = generateEntityFilter(hass, {
domain: "media_player",
entity_category: "none",
});
const hasMediaPlayers = Object.keys(hass.states).some(mediaPlayerFilter);
const summaryCards: LovelaceCardConfig[] = [
{
type: "heading",
heading: hass.localize("ui.panel.lovelace.strategy.home.summaries"),
},
{
type: "home-summary",
summary: "light",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/light?historyBack=1",
},
grid_options: {
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard,
{
type: "home-summary",
summary: "climate",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/climate?historyBack=1",
},
grid_options: {
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard,
{
type: "home-summary",
summary: "safety",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/safety?historyBack=1",
},
grid_options: {
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard,
];
// Only add media players summary if there are media player entities
if (hasMediaPlayers) {
summaryCards.push({
type: "home-summary",
summary: "media_players",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "media-players",
},
grid_options: {
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard);
}
const summarySection: LovelaceSectionConfig = {
type: "grid",
column_span: maxColumns,
cards: [
{
type: "heading",
heading: hass.localize("ui.panel.lovelace.strategy.home.summaries"),
},
{
type: "home-summary",
summary: "light",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/light?historyBack=1",
},
grid_options: {
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard,
{
type: "home-summary",
summary: "climate",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/climate?historyBack=1",
},
grid_options: {
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard,
{
type: "home-summary",
summary: "safety",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/safety?historyBack=1",
},
grid_options: {
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard,
{
type: "home-summary",
summary: "media_players",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "media-players",
},
grid_options: {
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard,
],
cards: summaryCards,
};
const weatherFilter = generateEntityFilter(hass, {

View File

@@ -59,6 +59,26 @@ const processAreasForMediaPlayers = (
return cards;
};
const processUnassignedEntities = (
hass: HomeAssistant,
entities: string[]
): LovelaceCardConfig[] => {
const unassignedFilter = generateEntityFilter(hass, {
area: null,
});
const unassignedEntities = entities.filter(unassignedFilter);
const areaCards: LovelaceCardConfig[] = [];
for (const entityId of unassignedEntities) {
areaCards.push({
type: "media-control",
entity: entityId,
} satisfies MediaControlCardConfig);
}
return areaCards;
};
@customElement("home-media-players-view-strategy")
export class HomeMMediaPlayersViewStrategy extends ReactiveElement {
static async generate(
@@ -134,10 +154,35 @@ export class HomeMMediaPlayersViewStrategy extends ReactiveElement {
}
}
// Process unassigned entities
const unassignedCards = processUnassignedEntities(hass, entities);
if (unassignedCards.length > 0) {
const section: LovelaceSectionRawConfig = {
type: "grid",
column_span: 2,
cards: [
{
type: "heading",
heading:
sections.length > 0
? hass.localize(
"ui.panel.lovelace.strategy.home_media_players.other_media_players"
)
: hass.localize(
"ui.panel.lovelace.strategy.home_media_players.media_players"
),
},
...unassignedCards,
],
};
sections.push(section);
}
return {
type: "sections",
max_columns: 2,
sections: sections || [],
sections: sections,
};
}
}

View File

@@ -29,8 +29,6 @@ import {
} from "../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../../components/ha-dropdown-item";
import "../../components/ha-dropdown";
// Client ID used by iOS app
const iOSclientId = "https://home-assistant.io/iOS";
@@ -148,18 +146,19 @@ class HaRefreshTokens extends LitElement {
)}
</div>
<div>
<ha-dropdown>
<ha-md-button-menu positioning="popover">
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item
<ha-md-menu-item
graphic="icon"
@click=${this._toggleTokenExpiration}
.token=${token}
>
<ha-svg-icon
slot="icon"
slot="start"
.path=${token.expire_at
? mdiClockRemoveOutline
: mdiClockCheckOutline}
@@ -171,20 +170,24 @@ class HaRefreshTokens extends LitElement {
: this.hass.localize(
"ui.panel.profile.refresh_tokens.enable_token_expiration"
)}
</ha-dropdown-item>
<ha-dropdown-item
variant="danger"
</ha-md-menu-item>
<ha-md-menu-item
graphic="icon"
class="warning"
.disabled=${token.is_current}
@click=${this._deleteToken}
.token=${token}
>
<ha-svg-icon
slot="icon"
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
${this.hass.localize("ui.common.delete")}
</ha-dropdown-item>
</ha-dropdown>
<div slot="headline">
${this.hass.localize("ui.common.delete")}
</div>
</ha-md-menu-item>
</ha-md-button-menu>
</div>
</ha-settings-row>
`

View File

@@ -103,6 +103,24 @@ const processAreasForSafety = (
return cards;
};
const processUnassignedEntities = (
hass: HomeAssistant,
entities: string[]
): LovelaceCardConfig[] => {
const unassignedFilter = generateEntityFilter(hass, {
area: null,
});
const unassignedLights = entities.filter(unassignedFilter);
const areaCards: LovelaceCardConfig[] = [];
const computeTileCard = computeAreaTileCardConfig(hass, "", false);
for (const entityId of unassignedLights) {
areaCards.push(computeTileCard(entityId));
}
return areaCards;
};
@customElement("safety-view-strategy")
export class SafetyViewStrategy extends ReactiveElement {
static async generate(
@@ -178,10 +196,33 @@ export class SafetyViewStrategy extends ReactiveElement {
}
}
// Process unassigned entities
const unassignedCards = processUnassignedEntities(hass, entities);
if (unassignedCards.length > 0) {
const section: LovelaceSectionRawConfig = {
type: "grid",
column_span: 2,
cards: [
{
type: "heading",
heading:
sections.length > 0
? hass.localize(
"ui.panel.lovelace.strategy.safety.other_devices"
)
: hass.localize("ui.panel.lovelace.strategy.safety.devices"),
},
...unassignedCards,
],
};
sections.push(section);
}
return {
type: "sections",
max_columns: 2,
sections: sections || [],
sections: sections,
};
}
}

View File

@@ -155,10 +155,6 @@ export const semanticColorStyles = css`
/* Surfaces */
--ha-color-surface-default: var(--ha-color-neutral-95);
--ha-color-on-surface-default: var(--ha-color-neutral-05);
/* shadow */
--ha-color-shadow: rgba(0, 0, 0, 0.2);
}
`;
@@ -290,9 +286,5 @@ export const darkSemanticColorStyles = css`
/* Surfaces */
--ha-color-surface-default: var(--ha-color-neutral-10);
--ha-color-on-surface-default: var(--ha-color-neutral-95);
/* shadow */
--ha-color-shadow: rgba(255, 255, 255, 0.2);
}
`;

View File

@@ -52,9 +52,7 @@ export const waColorStyles = css`
--wa-color-danger-on-normal: var(--ha-color-on-danger-normal);
--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-raised: var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff));
--wa-color-surface-default: var(--card-background-color);
--wa-panel-border-radius: var(--ha-border-radius-3xl);
--wa-panel-border-style: solid;
--wa-panel-border-width: 1px;

View File

@@ -16,17 +16,7 @@ export const waMainStyles = css`
--wa-font-weight-action: var(--ha-font-weight-medium);
--wa-transition-fast: 75ms;
--wa-transition-easing: ease;
--wa-border-style: solid;
--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: 1.25;
--wa-border-width-l: var(--ha-border-radius-lg);
--wa-space-xl: 32px;
}

View File

@@ -6975,6 +6975,22 @@
"common_controls": {
"not_loaded": "Usage Prediction integration is not loaded.",
"no_data": "This place will soon fill up with the entities you use most often, based on your activity."
},
"light": {
"lights": "Lights",
"other_lights": "Other lights"
},
"safety": {
"devices": "Devices",
"other_devices": "Other devices"
},
"climate": {
"devices": "Devices",
"other_devices": "Other devices"
},
"home_media_players": {
"media_players": "Media players",
"other_media_players": "Other media players"
}
},
"cards": {
@@ -7533,6 +7549,12 @@
"state_equal": "State is equal to",
"state_not_equal": "State is not equal to"
},
"time": {
"label": "Time",
"after": "After",
"before": "Before",
"weekdays": "Weekdays"
},
"location": {
"label": "Location",
"locations": "Locations",

View File

@@ -0,0 +1,209 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { checkTimeInRange } from "../../../src/common/datetime/check_time";
import type { HomeAssistant } from "../../../src/types";
import {
NumberFormat,
TimeFormat,
FirstWeekday,
DateFormat,
TimeZone,
} from "../../../src/data/translation";
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, "08:00", "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, "08:00", "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, "08:00", "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, "22:00", "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, "22:00", "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, "22:00", "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, "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, "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, undefined, "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, undefined, "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, undefined, undefined, ["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, undefined, undefined, ["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, undefined, undefined, ["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, "08:00", "17:00", ["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, "08:00", "17:00", ["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, "08:00", "17:00", ["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)).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, "01:00", "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, "01:00", "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, "01:00", "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, "22:00", "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, "08:00", "17:00")).toBe(true);
expect(checkTimeInRange(mockHass, "12:00", "17:00")).toBe(false);
});
});
});

View File

@@ -388,4 +388,70 @@ describe("generateEntityFilter", () => {
expect(filter("light.no_area")).toBe(false);
});
});
describe("null filtering", () => {
it("should filter entities with no area when null is used", () => {
const filter = generateEntityFilter(mockHass, { area: null });
expect(filter("light.no_area")).toBe(true);
expect(filter("light.living_room")).toBe(false);
});
it("should filter entities with specific area OR no area when null is in array", () => {
const filter = generateEntityFilter(mockHass, {
area: ["living_room", null],
});
expect(filter("light.living_room")).toBe(true);
expect(filter("sensor.temperature")).toBe(true);
expect(filter("light.no_area")).toBe(true);
expect(filter("switch.kitchen")).toBe(false);
});
it("should filter entities with no floor when null is used", () => {
const filter = generateEntityFilter(mockHass, { floor: null });
expect(filter("light.no_area")).toBe(true);
expect(filter("light.living_room")).toBe(false);
});
it("should filter entities with specific floor OR no floor", () => {
const filter = generateEntityFilter(mockHass, {
floor: ["main_floor", null],
});
expect(filter("light.living_room")).toBe(true);
expect(filter("switch.kitchen")).toBe(true);
expect(filter("light.no_area")).toBe(true);
expect(filter("light.bedroom")).toBe(false);
});
it("should filter entities with no device when null is used", () => {
const filter = generateEntityFilter(mockHass, { device: null });
expect(filter("light.living_room")).toBe(false);
expect(filter("light.no_area")).toBe(false);
});
it("should filter entities with specific device OR no device", () => {
const filter = generateEntityFilter(mockHass, {
device: ["device1", null],
});
expect(filter("light.living_room")).toBe(true);
expect(filter("switch.kitchen")).toBe(false);
});
it("should combine null filtering with other criteria", () => {
const filter = generateEntityFilter(mockHass, {
domain: "light",
area: ["living_room", null],
});
expect(filter("light.living_room")).toBe(true);
expect(filter("light.no_area")).toBe(true);
expect(filter("light.bedroom")).toBe(false);
expect(filter("sensor.temperature")).toBe(false);
});
});
});

118
yarn.lock
View File

@@ -3446,18 +3446,18 @@ __metadata:
languageName: node
linkType: hard
"@octokit/core@npm:^7.0.2":
version: 7.0.4
resolution: "@octokit/core@npm:7.0.4"
"@octokit/core@npm:^7.0.6":
version: 7.0.6
resolution: "@octokit/core@npm:7.0.6"
dependencies:
"@octokit/auth-token": "npm:^6.0.0"
"@octokit/graphql": "npm:^9.0.1"
"@octokit/request": "npm:^10.0.2"
"@octokit/request-error": "npm:^7.0.0"
"@octokit/types": "npm:^15.0.0"
"@octokit/graphql": "npm:^9.0.3"
"@octokit/request": "npm:^10.0.6"
"@octokit/request-error": "npm:^7.0.2"
"@octokit/types": "npm:^16.0.0"
before-after-hook: "npm:^4.0.0"
universal-user-agent: "npm:^7.0.0"
checksum: 10/d691df211ba9a2941ec97dc32e1c34e26e7c8161425fb573425cfc2727f548bf2a2445eec9d36748b4b469478feb21de4bec1c00a34aa02eedf5b7625ca52189
checksum: 10/852d41fc3150d2a891156427dd0575c77889f1c7a109894ee541594e3fd47c0d4e0a93fee22966c507dfd6158b522e42846c2ac46b9d896078194c95fa81f4ae
languageName: node
linkType: hard
@@ -3471,14 +3471,14 @@ __metadata:
languageName: node
linkType: hard
"@octokit/graphql@npm:^9.0.1":
version: 9.0.1
resolution: "@octokit/graphql@npm:9.0.1"
"@octokit/graphql@npm:^9.0.3":
version: 9.0.3
resolution: "@octokit/graphql@npm:9.0.3"
dependencies:
"@octokit/request": "npm:^10.0.2"
"@octokit/types": "npm:^14.0.0"
"@octokit/request": "npm:^10.0.6"
"@octokit/types": "npm:^16.0.0"
universal-user-agent: "npm:^7.0.0"
checksum: 10/02d7ea4e2c17a4d4b7311150d0326318c756aff6cf955d9ba443a4bf26b32784832060379fc74f4537657415b262c10adb7f4a1655e15b143d19c2f099b87f16
checksum: 10/7b16f281f8571dce55280b3986fbb8d15465a7236164a5f6497ded7597ff9ee95d5796924555b979903fe8c6706fe6be1b3e140d807297f85ac8edeadc28f9fe
languageName: node
linkType: hard
@@ -3501,20 +3501,6 @@ __metadata:
languageName: node
linkType: hard
"@octokit/openapi-types@npm:^25.1.0":
version: 25.1.0
resolution: "@octokit/openapi-types@npm:25.1.0"
checksum: 10/91989a4cec12250e6b3226e9aa931c05c27d46a946725d01e6a831af3890f157210a7032f07641a156c608cc6bf6cf55a28f07179910b644966358d6d559dec6
languageName: node
linkType: hard
"@octokit/openapi-types@npm:^26.0.0":
version: 26.0.0
resolution: "@octokit/openapi-types@npm:26.0.0"
checksum: 10/b9e1b1230b0a3d280b48902a927ce4e7df0d51096c928e2ee929035b0bce779fe7748a1ae58696f1c3080bf8338b6388d5caba5b0dbf254e9713303ed3abf7c2
languageName: node
linkType: hard
"@octokit/openapi-types@npm:^27.0.0":
version: 27.0.0
resolution: "@octokit/openapi-types@npm:27.0.0"
@@ -3522,14 +3508,14 @@ __metadata:
languageName: node
linkType: hard
"@octokit/plugin-paginate-rest@npm:^13.0.1":
version: 13.1.1
resolution: "@octokit/plugin-paginate-rest@npm:13.1.1"
"@octokit/plugin-paginate-rest@npm:^14.0.0":
version: 14.0.0
resolution: "@octokit/plugin-paginate-rest@npm:14.0.0"
dependencies:
"@octokit/types": "npm:^14.1.0"
"@octokit/types": "npm:^16.0.0"
peerDependencies:
"@octokit/core": ">=6"
checksum: 10/26b9b7a233b77fff31d31469879a281e651417df86799387d6563446f037c9969061b833ab0698857c9797ea856f2ef7ed6970d8fb471239879c9298bbabe200
checksum: 10/57ddd857528dad9c02431bc6254c2374c06057872cf9656a4a88b162ebe1c2bc9f34fbec360f2ccff72c940f29b120758ce14e8135bd027223d381eb1b8b6579
languageName: node
linkType: hard
@@ -3542,14 +3528,14 @@ __metadata:
languageName: node
linkType: hard
"@octokit/plugin-rest-endpoint-methods@npm:^16.0.0":
version: 16.1.0
resolution: "@octokit/plugin-rest-endpoint-methods@npm:16.1.0"
"@octokit/plugin-rest-endpoint-methods@npm:^17.0.0":
version: 17.0.0
resolution: "@octokit/plugin-rest-endpoint-methods@npm:17.0.0"
dependencies:
"@octokit/types": "npm:^15.0.0"
"@octokit/types": "npm:^16.0.0"
peerDependencies:
"@octokit/core": ">=6"
checksum: 10/9b62d1ddf3435d77cbf11f36abf951b755cf2033dfd5f1875d35f97ad4ab9391e7d0e008ae5b813b7fe0b6c537ab38240b4911c61b53cf8fb8d9957a3d898e09
checksum: 10/e9d9ad4d9755cc7fb82fdcbfa870ddea8a432180f0f76c8469095557fd1e26f8caea8cae58401209be17c4f3d8cc48c0e16a3643e37e48f4d23c39e058bf2c55
languageName: node
linkType: hard
@@ -3566,7 +3552,7 @@ __metadata:
languageName: node
linkType: hard
"@octokit/request-error@npm:^7.0.0, @octokit/request-error@npm:^7.0.2":
"@octokit/request-error@npm:^7.0.2":
version: 7.0.2
resolution: "@octokit/request-error@npm:7.0.2"
dependencies:
@@ -3575,7 +3561,7 @@ __metadata:
languageName: node
linkType: hard
"@octokit/request@npm:^10.0.2, @octokit/request@npm:^10.0.6":
"@octokit/request@npm:^10.0.6":
version: 10.0.6
resolution: "@octokit/request@npm:10.0.6"
dependencies:
@@ -3588,33 +3574,15 @@ __metadata:
languageName: node
linkType: hard
"@octokit/rest@npm:22.0.0":
version: 22.0.0
resolution: "@octokit/rest@npm:22.0.0"
"@octokit/rest@npm:22.0.1":
version: 22.0.1
resolution: "@octokit/rest@npm:22.0.1"
dependencies:
"@octokit/core": "npm:^7.0.2"
"@octokit/plugin-paginate-rest": "npm:^13.0.1"
"@octokit/core": "npm:^7.0.6"
"@octokit/plugin-paginate-rest": "npm:^14.0.0"
"@octokit/plugin-request-log": "npm:^6.0.0"
"@octokit/plugin-rest-endpoint-methods": "npm:^16.0.0"
checksum: 10/d2b80fefd6aed307cb728980cb1d94cb484d48fabf0055198664287a7fb50544d312b005e4fb8dec2a6e97a153ec0ad7654d62f59898e1077a4cfba64e6d5c3e
languageName: node
linkType: hard
"@octokit/types@npm:^14.0.0, @octokit/types@npm:^14.1.0":
version: 14.1.0
resolution: "@octokit/types@npm:14.1.0"
dependencies:
"@octokit/openapi-types": "npm:^25.1.0"
checksum: 10/ea5549ca6176bd1184427141a77bca88c68f07d252d3ea1db7f9b58ec16b66391218a75a99927efb1e36a2cb00e8ed37a79b71fdc95a1117a9982516156fd997
languageName: node
linkType: hard
"@octokit/types@npm:^15.0.0":
version: 15.0.0
resolution: "@octokit/types@npm:15.0.0"
dependencies:
"@octokit/openapi-types": "npm:^26.0.0"
checksum: 10/c9207551ea0a56f7b740d7fed7f0eb3801abfc39b67ece6f914ffc41484879342cf909300c601ccb1ea5735737491f539c9a1464ddbc35acad323f6d78b6f17f
"@octokit/plugin-rest-endpoint-methods": "npm:^17.0.0"
checksum: 10/ec2e94cfa8766716faeb3ca18527d9af746482d35aaa6e4265a30cb669ae3f31f4ebb6235edebe5ae62bc2cec2b8e88902584f698df2e7cabac3a15fd27da665
languageName: node
linkType: hard
@@ -8971,10 +8939,10 @@ __metadata:
languageName: node
linkType: hard
"globals@npm:16.4.0":
version: 16.4.0
resolution: "globals@npm:16.4.0"
checksum: 10/1627a9f42fb4c82d7af6a0c8b6cd616e00110908304d5f1ddcdf325998f3aed45a4b29d8a1e47870f328817805263e31e4f1673f00022b9c2b210552767921cf
"globals@npm:16.5.0":
version: 16.5.0
resolution: "globals@npm:16.5.0"
checksum: 10/f9e8a2a13f50222c127030a619e283e7bbfe32966316bdde0715af1d15a7e40cb9c24ff52cad59671f97762ed8b515353c2f8674f560c63d9385f19ee26735a6
languageName: node
linkType: hard
@@ -9216,10 +9184,10 @@ __metadata:
languageName: node
linkType: hard
"hls.js@npm:1.6.13":
version: 1.6.13
resolution: "hls.js@npm:1.6.13"
checksum: 10/4de045fddbeb6edc3859021ff60b268a642aa87e83347a3a764e53299b616d57f36eecef0b91eab240ffde84f75d1ab5f7cc0916a929de265707d0f0b27d1c71
"hls.js@npm:1.6.14":
version: 1.6.14
resolution: "hls.js@npm:1.6.14"
checksum: 10/56eedf163912abf72bd5ca0eac44dbaa442cff59bb8d1ff3303614bf18d21dfa3ec7b85db5b71449986c52c63b7efb6bd5392b048bf22c61549f71a001fa94cd
languageName: node
linkType: hard
@@ -9293,7 +9261,7 @@ __metadata:
"@mdi/svg": "npm:7.4.47"
"@octokit/auth-oauth-device": "npm:8.0.3"
"@octokit/plugin-retry": "npm:8.0.3"
"@octokit/rest": "npm:22.0.0"
"@octokit/rest": "npm:22.0.1"
"@replit/codemirror-indentation-markers": "npm:6.5.3"
"@rsdoctor/rspack-plugin": "npm:1.3.7"
"@rspack/core": "npm:1.6.0"
@@ -9363,7 +9331,7 @@ __metadata:
gulp-json-transform: "npm:0.5.0"
gulp-rename: "npm:2.1.0"
gulp-zopfli-green: "npm:6.0.2"
hls.js: "npm:1.6.13"
hls.js: "npm:1.6.14"
home-assistant-js-websocket: "npm:9.5.0"
html-minifier-terser: "npm:7.2.0"
husky: "npm:9.1.7"