mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-06 17:40:27 +00:00
Compare commits
19 Commits
view-trans
...
card-visib
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d325b5627 | ||
|
|
ef0368f9c3 | ||
|
|
8fc305ec6b | ||
|
|
fd657fbf68 | ||
|
|
885f4564a9 | ||
|
|
d27c2e3a04 | ||
|
|
f2655ec226 | ||
|
|
1c3043ed37 | ||
|
|
86bfb6a482 | ||
|
|
bd3a1ea281 | ||
|
|
90757b6f97 | ||
|
|
6a2bbb02a2 | ||
|
|
698197dccd | ||
|
|
665ff88d58 | ||
|
|
bb8dd69e7b | ||
|
|
819182977c | ||
|
|
2d838e22d2 | ||
|
|
036cbe0a1a | ||
|
|
6fc039340e |
@@ -18,16 +18,16 @@ module.exports.sourceMapURL = () => {
|
||||
module.exports.ignorePackages = () => [];
|
||||
|
||||
// Files from NPM packages that we should replace with empty file
|
||||
module.exports.emptyPackages = ({ isHassioBuild, isLandingPageBuild }) =>
|
||||
module.exports.emptyPackages = ({ isHassioBuild }) =>
|
||||
[
|
||||
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
|
||||
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
|
||||
// Icons in supervisor conflict with icons in HA so we don't load.
|
||||
(isHassioBuild || isLandingPageBuild) &&
|
||||
isHassioBuild &&
|
||||
require.resolve(
|
||||
path.resolve(paths.root_dir, "src/components/ha-icon.ts")
|
||||
),
|
||||
(isHassioBuild || isLandingPageBuild) &&
|
||||
isHassioBuild &&
|
||||
require.resolve(
|
||||
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
|
||||
),
|
||||
@@ -337,7 +337,6 @@ module.exports.config = {
|
||||
publicPath: publicPath(latestBuild),
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isLandingPageBuild: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -41,7 +41,6 @@ const createRspackConfig = ({
|
||||
isStatsBuild,
|
||||
isTestBuild,
|
||||
isHassioBuild,
|
||||
isLandingPageBuild,
|
||||
dontHash,
|
||||
}) => {
|
||||
if (!dontHash) {
|
||||
@@ -169,9 +168,7 @@ const createRspackConfig = ({
|
||||
},
|
||||
}),
|
||||
new rspack.NormalModuleReplacementPlugin(
|
||||
new RegExp(
|
||||
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).join("|")
|
||||
),
|
||||
new RegExp(bundle.emptyPackages({ isHassioBuild }).join("|")),
|
||||
path.resolve(paths.root_dir, "src/util/empty.js")
|
||||
),
|
||||
!isProdBuild && new LogStartCompilePlugin(),
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import "@material/mwc-linear-progress";
|
||||
import { mdiOpenInNew } from "@mdi/js";
|
||||
import { css, html, nothing, type PropertyValues } from "lit";
|
||||
import { type PropertyValues, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { extractSearchParam } from "../../src/common/url/search-params";
|
||||
import "../../src/components/ha-alert";
|
||||
import "../../src/components/ha-button";
|
||||
import "../../src/components/ha-fade-in";
|
||||
import "../../src/components/ha-spinner";
|
||||
import "../../src/components/ha-svg-icon";
|
||||
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
||||
import "../../src/onboarding/onboarding-welcome-links";
|
||||
import { onBoardingStyles } from "../../src/onboarding/styles";
|
||||
import { haStyle } from "../../src/resources/styles";
|
||||
import "./components/landing-page-logs";
|
||||
import "../../src/onboarding/onboarding-welcome-links";
|
||||
import "./components/landing-page-network";
|
||||
import "./components/landing-page-logs";
|
||||
import { extractSearchParam } from "../../src/common/url/search-params";
|
||||
import { onBoardingStyles } from "../../src/onboarding/styles";
|
||||
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
||||
import { LandingPageBaseElement } from "./landing-page-base-element";
|
||||
import {
|
||||
getSupervisorNetworkInfo,
|
||||
pingSupervisor,
|
||||
type NetworkInfo,
|
||||
} from "./data/supervisor";
|
||||
import { LandingPageBaseElement } from "./landing-page-base-element";
|
||||
|
||||
export const ASSUME_CORE_START_SECONDS = 60;
|
||||
const SCHEDULE_CORE_CHECK_SECONDS = 1;
|
||||
@@ -97,21 +94,16 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
<ha-language-picker
|
||||
.value=${this.language}
|
||||
.label=${""}
|
||||
button-style
|
||||
native-name
|
||||
@value-changed=${this._languageChanged}
|
||||
inline-arrow
|
||||
></ha-language-picker>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
variant="neutral"
|
||||
<a
|
||||
href="https://www.home-assistant.io/getting-started/onboarding/"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>${this.localize("ui.panel.page-onboarding.help")}</a
|
||||
>
|
||||
${this.localize("ui.panel.page-onboarding.help")}
|
||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -226,8 +218,26 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
ha-alert p {
|
||||
text-align: unset;
|
||||
}
|
||||
.footer ha-svg-icon {
|
||||
--mdc-icon-size: var(--ha-space-5);
|
||||
ha-language-picker {
|
||||
display: block;
|
||||
width: 200px;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
overflow: hidden;
|
||||
--ha-select-height: 40px;
|
||||
--mdc-select-fill-color: none;
|
||||
--mdc-select-label-ink-color: var(--primary-text-color, #212121);
|
||||
--mdc-select-ink-color: var(--primary-text-color, #212121);
|
||||
--mdc-select-idle-line-color: transparent;
|
||||
--mdc-select-hover-line-color: transparent;
|
||||
--mdc-select-dropdown-icon-color: var(--primary-text-color, #212121);
|
||||
--mdc-shape-small: 0;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
margin-right: 16px;
|
||||
margin-inline-end: 16px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
ha-fade-in {
|
||||
min-height: calc(100vh - 64px - 88px);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/* eslint-disable lit/prefer-static-styles */
|
||||
import { mdiOpenInNew } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -7,8 +6,6 @@ import punycode from "punycode";
|
||||
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
|
||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||
import "../components/ha-alert";
|
||||
import "../components/ha-button";
|
||||
import "../components/ha-svg-icon";
|
||||
import type { AuthProvider, AuthUrlSearchParams } from "../data/auth";
|
||||
import { fetchAuthProviders } from "../data/auth";
|
||||
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
||||
@@ -136,8 +133,25 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.footer ha-svg-icon {
|
||||
--mdc-icon-size: var(--ha-space-5);
|
||||
ha-language-picker {
|
||||
width: 200px;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
overflow: hidden;
|
||||
--ha-select-height: 40px;
|
||||
--mdc-select-fill-color: none;
|
||||
--mdc-select-label-ink-color: var(--primary-text-color, #212121);
|
||||
--mdc-select-ink-color: var(--primary-text-color, #212121);
|
||||
--mdc-select-idle-line-color: transparent;
|
||||
--mdc-select-hover-line-color: transparent;
|
||||
--mdc-select-dropdown-icon-color: var(--primary-text-color, #212121);
|
||||
--mdc-shape-small: 0;
|
||||
}
|
||||
.footer a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
margin-right: 16px;
|
||||
margin-inline-end: 16px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
h1 {
|
||||
font-size: var(--ha-font-size-3xl);
|
||||
@@ -191,21 +205,16 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
<ha-language-picker
|
||||
.value=${this.language}
|
||||
.label=${""}
|
||||
button-style
|
||||
native-name
|
||||
@value-changed=${this._languageChanged}
|
||||
inline-arrow
|
||||
></ha-language-picker>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
variant="neutral"
|
||||
<a
|
||||
href="https://www.home-assistant.io/docs/authentication/"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>${this.localize("ui.panel.page-authorize.help")}</a
|
||||
>
|
||||
${this.localize("ui.panel.page-authorize.help")}
|
||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
110
src/common/datetime/check_time.ts
Normal file
110
src/common/datetime/check_time.ts
Normal 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;
|
||||
};
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
59
src/common/datetime/weekday.ts
Normal file
59
src/common/datetime/weekday.ts
Normal 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>;
|
||||
@@ -10,6 +10,7 @@ import type { HomeAssistant } from "../types";
|
||||
import "./ha-bottom-sheet";
|
||||
import "./ha-button";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-picker-combo-box";
|
||||
import type {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { mdiMenuDown } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { formatLanguageCode } from "../common/language/format_language";
|
||||
@@ -9,10 +8,10 @@ import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import type { FrontendLocaleData } from "../data/translation";
|
||||
import { translationMetadata } from "../resources/translations-metadata";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./ha-button";
|
||||
import "./ha-generic-picker";
|
||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||
import "./ha-list-item";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
import "./ha-select";
|
||||
|
||||
export const getLanguageOptions = (
|
||||
languages: string[],
|
||||
@@ -76,9 +75,6 @@ export class HaLanguagePicker extends LitElement {
|
||||
@property({ attribute: "native-name", type: Boolean })
|
||||
public nativeName = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "button-style" })
|
||||
public buttonStyle = false;
|
||||
|
||||
@property({ attribute: "no-sort", type: Boolean }) public noSort = false;
|
||||
|
||||
@property({ attribute: "inline-arrow", type: Boolean })
|
||||
@@ -86,8 +82,6 @@ export class HaLanguagePicker extends LitElement {
|
||||
|
||||
@state() _defaultLanguages: string[] = [];
|
||||
|
||||
@query("ha-generic-picker", true) public genericPicker!: HaGenericPicker;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._computeDefaultLanguageOptions();
|
||||
@@ -107,13 +101,12 @@ export class HaLanguagePicker extends LitElement {
|
||||
this.hass?.locale
|
||||
);
|
||||
|
||||
private _getLanguageName = (lang?: string) =>
|
||||
this._getItems().find((language) => language.id === lang)?.primary;
|
||||
|
||||
private _valueRenderer = (value) =>
|
||||
html`<span slot="headline"
|
||||
>${this._getLanguageName(value) ?? value}</span
|
||||
> `;
|
||||
private _valueRenderer = (value) => {
|
||||
const language = this._getItems().find(
|
||||
(lang) => lang.id === value
|
||||
)?.primary;
|
||||
return html`<span slot="headline">${language ?? value}</span> `;
|
||||
};
|
||||
|
||||
protected render() {
|
||||
const value =
|
||||
@@ -137,28 +130,10 @@ export class HaLanguagePicker extends LitElement {
|
||||
.getItems=${this._getItems}
|
||||
@value-changed=${this._changed}
|
||||
hide-clear-icon
|
||||
>
|
||||
${this.buttonStyle
|
||||
? html`<ha-button
|
||||
slot="field"
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._openPicker}
|
||||
appearance="plain"
|
||||
variant="neutral"
|
||||
>
|
||||
${this._getLanguageName(value)}
|
||||
<ha-svg-icon slot="end" .path=${mdiMenuDown}></ha-svg-icon>
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</ha-generic-picker>
|
||||
></ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _openPicker(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this.genericPicker.open();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-generic-picker {
|
||||
width: 100%;
|
||||
|
||||
@@ -87,208 +87,166 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
protected render() {
|
||||
if (this.addOnTop) {
|
||||
return html` ${this._renderPicker()} ${this._renderItems()} `;
|
||||
return html` ${this._renderChips()} ${this._renderItems()} `;
|
||||
}
|
||||
return html` ${this._renderItems()} ${this._renderPicker()} `;
|
||||
return html` ${this._renderItems()} ${this._renderChips()} `;
|
||||
}
|
||||
|
||||
private _renderValueChips() {
|
||||
const entityIds = this.value?.entity_id
|
||||
? ensureArray(this.value.entity_id)
|
||||
: [];
|
||||
const deviceIds = this.value?.device_id
|
||||
? ensureArray(this.value.device_id)
|
||||
: [];
|
||||
const areaIds = this.value?.area_id ? ensureArray(this.value.area_id) : [];
|
||||
const floorIds = this.value?.floor_id
|
||||
? ensureArray(this.value.floor_id)
|
||||
: [];
|
||||
const labelIds = this.value?.label_id
|
||||
? ensureArray(this.value.label_id)
|
||||
: [];
|
||||
|
||||
if (
|
||||
!entityIds.length &&
|
||||
!deviceIds.length &&
|
||||
!areaIds.length &&
|
||||
!floorIds.length &&
|
||||
!labelIds.length
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="mdc-chip-set items">
|
||||
${floorIds.length
|
||||
? floorIds.map(
|
||||
(floor_id) => html`
|
||||
<ha-target-picker-value-chip
|
||||
.hass=${this.hass}
|
||||
type="floor"
|
||||
.itemId=${floor_id}
|
||||
@remove-target-item=${this._handleRemove}
|
||||
@expand-target-item=${this._handleExpand}
|
||||
></ha-target-picker-value-chip>
|
||||
`
|
||||
)
|
||||
: nothing}
|
||||
${areaIds.length
|
||||
? areaIds.map(
|
||||
(area_id) => html`
|
||||
<ha-target-picker-value-chip
|
||||
.hass=${this.hass}
|
||||
type="area"
|
||||
.itemId=${area_id}
|
||||
@remove-target-item=${this._handleRemove}
|
||||
@expand-target-item=${this._handleExpand}
|
||||
></ha-target-picker-value-chip>
|
||||
`
|
||||
)
|
||||
: nothing}
|
||||
${deviceIds.length
|
||||
? deviceIds.map(
|
||||
(device_id) => html`
|
||||
<ha-target-picker-value-chip
|
||||
.hass=${this.hass}
|
||||
type="device"
|
||||
.itemId=${device_id}
|
||||
@remove-target-item=${this._handleRemove}
|
||||
@expand-target-item=${this._handleExpand}
|
||||
></ha-target-picker-value-chip>
|
||||
`
|
||||
)
|
||||
: nothing}
|
||||
${entityIds.length
|
||||
? entityIds.map(
|
||||
(entity_id) => html`
|
||||
<ha-target-picker-value-chip
|
||||
.hass=${this.hass}
|
||||
type="entity"
|
||||
.itemId=${entity_id}
|
||||
@remove-target-item=${this._handleRemove}
|
||||
@expand-target-item=${this._handleExpand}
|
||||
></ha-target-picker-value-chip>
|
||||
`
|
||||
)
|
||||
: nothing}
|
||||
${labelIds.length
|
||||
? labelIds.map(
|
||||
(label_id) => html`
|
||||
<ha-target-picker-value-chip
|
||||
.hass=${this.hass}
|
||||
type="label"
|
||||
.itemId=${label_id}
|
||||
@remove-target-item=${this._handleRemove}
|
||||
@expand-target-item=${this._handleExpand}
|
||||
></ha-target-picker-value-chip>
|
||||
`
|
||||
)
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
return html`<div class="mdc-chip-set items">
|
||||
${this.value?.floor_id
|
||||
? ensureArray(this.value.floor_id).map(
|
||||
(floor_id) => html`
|
||||
<ha-target-picker-value-chip
|
||||
.hass=${this.hass}
|
||||
type="floor"
|
||||
.itemId=${floor_id}
|
||||
@remove-target-item=${this._handleRemove}
|
||||
@expand-target-item=${this._handleExpand}
|
||||
></ha-target-picker-value-chip>
|
||||
`
|
||||
)
|
||||
: nothing}
|
||||
${this.value?.area_id
|
||||
? ensureArray(this.value.area_id).map(
|
||||
(area_id) => html`
|
||||
<ha-target-picker-value-chip
|
||||
.hass=${this.hass}
|
||||
type="area"
|
||||
.itemId=${area_id}
|
||||
@remove-target-item=${this._handleRemove}
|
||||
@expand-target-item=${this._handleExpand}
|
||||
></ha-target-picker-value-chip>
|
||||
`
|
||||
)
|
||||
: nothing}
|
||||
${this.value?.device_id
|
||||
? ensureArray(this.value.device_id).map(
|
||||
(device_id) => html`
|
||||
<ha-target-picker-value-chip
|
||||
.hass=${this.hass}
|
||||
type="device"
|
||||
.itemId=${device_id}
|
||||
@remove-target-item=${this._handleRemove}
|
||||
@expand-target-item=${this._handleExpand}
|
||||
></ha-target-picker-value-chip>
|
||||
`
|
||||
)
|
||||
: nothing}
|
||||
${this.value?.entity_id
|
||||
? ensureArray(this.value.entity_id).map(
|
||||
(entity_id) => html`
|
||||
<ha-target-picker-value-chip
|
||||
.hass=${this.hass}
|
||||
type="entity"
|
||||
.itemId=${entity_id}
|
||||
@remove-target-item=${this._handleRemove}
|
||||
@expand-target-item=${this._handleExpand}
|
||||
></ha-target-picker-value-chip>
|
||||
`
|
||||
)
|
||||
: nothing}
|
||||
${this.value?.label_id
|
||||
? ensureArray(this.value.label_id).map(
|
||||
(label_id) => html`
|
||||
<ha-target-picker-value-chip
|
||||
.hass=${this.hass}
|
||||
type="label"
|
||||
.itemId=${label_id}
|
||||
@remove-target-item=${this._handleRemove}
|
||||
@expand-target-item=${this._handleExpand}
|
||||
></ha-target-picker-value-chip>
|
||||
`
|
||||
)
|
||||
: nothing}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderValueGroups() {
|
||||
const entityIds = this.value?.entity_id
|
||||
? ensureArray(this.value.entity_id)
|
||||
: [];
|
||||
const deviceIds = this.value?.device_id
|
||||
? ensureArray(this.value.device_id)
|
||||
: [];
|
||||
const areaIds = this.value?.area_id ? ensureArray(this.value.area_id) : [];
|
||||
const floorIds = this.value?.floor_id
|
||||
? ensureArray(this.value.floor_id)
|
||||
: [];
|
||||
const labelIds = this.value?.label_id
|
||||
? ensureArray(this.value?.label_id)
|
||||
: [];
|
||||
return html`<div class="item-groups">
|
||||
${this.value?.entity_id
|
||||
? html`
|
||||
<ha-target-picker-item-group
|
||||
@remove-target-item=${this._handleRemove}
|
||||
type="entity"
|
||||
.hass=${this.hass}
|
||||
.items=${{ entity: ensureArray(this.value?.entity_id) }}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
>
|
||||
</ha-target-picker-item-group>
|
||||
`
|
||||
: nothing}
|
||||
${this.value?.device_id
|
||||
? html`
|
||||
<ha-target-picker-item-group
|
||||
@remove-target-item=${this._handleRemove}
|
||||
type="device"
|
||||
.hass=${this.hass}
|
||||
.items=${{ device: ensureArray(this.value?.device_id) }}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
>
|
||||
</ha-target-picker-item-group>
|
||||
`
|
||||
: nothing}
|
||||
${this.value?.floor_id || this.value?.area_id
|
||||
? html`
|
||||
<ha-target-picker-item-group
|
||||
@remove-target-item=${this._handleRemove}
|
||||
type="area"
|
||||
.hass=${this.hass}
|
||||
.items=${{
|
||||
floor: ensureArray(this.value?.floor_id),
|
||||
area: ensureArray(this.value?.area_id),
|
||||
}}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
>
|
||||
</ha-target-picker-item-group>
|
||||
`
|
||||
: nothing}
|
||||
${this.value?.label_id
|
||||
? html`
|
||||
<ha-target-picker-item-group
|
||||
@remove-target-item=${this._handleRemove}
|
||||
type="label"
|
||||
.hass=${this.hass}
|
||||
.items=${{ label: ensureArray(this.value?.label_id) }}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
>
|
||||
</ha-target-picker-item-group>
|
||||
`
|
||||
: nothing}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderItems() {
|
||||
if (
|
||||
!entityIds.length &&
|
||||
!deviceIds.length &&
|
||||
!areaIds.length &&
|
||||
!floorIds.length &&
|
||||
!labelIds.length
|
||||
!this.value?.floor_id &&
|
||||
!this.value?.area_id &&
|
||||
!this.value?.device_id &&
|
||||
!this.value?.entity_id &&
|
||||
!this.value?.label_id
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="item-groups">
|
||||
${entityIds.length
|
||||
? html`
|
||||
<ha-target-picker-item-group
|
||||
@remove-target-item=${this._handleRemove}
|
||||
type="entity"
|
||||
.hass=${this.hass}
|
||||
.items=${{ entity: entityIds }}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
>
|
||||
</ha-target-picker-item-group>
|
||||
`
|
||||
: nothing}
|
||||
${deviceIds.length
|
||||
? html`
|
||||
<ha-target-picker-item-group
|
||||
@remove-target-item=${this._handleRemove}
|
||||
type="device"
|
||||
.hass=${this.hass}
|
||||
.items=${{ device: deviceIds }}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
>
|
||||
</ha-target-picker-item-group>
|
||||
`
|
||||
: nothing}
|
||||
${floorIds.length || areaIds.length
|
||||
? html`
|
||||
<ha-target-picker-item-group
|
||||
@remove-target-item=${this._handleRemove}
|
||||
type="area"
|
||||
.hass=${this.hass}
|
||||
.items=${{
|
||||
floor: floorIds,
|
||||
area: areaIds,
|
||||
}}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
>
|
||||
</ha-target-picker-item-group>
|
||||
`
|
||||
: nothing}
|
||||
${labelIds.length
|
||||
? html`
|
||||
<ha-target-picker-item-group
|
||||
@remove-target-item=${this._handleRemove}
|
||||
type="label"
|
||||
.hass=${this.hass}
|
||||
.items=${{ label: labelIds }}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
>
|
||||
</ha-target-picker-item-group>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderItems() {
|
||||
return html`
|
||||
${this.compact ? this._renderValueChips() : this._renderValueGroups()}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPicker() {
|
||||
private _renderChips() {
|
||||
return html`
|
||||
<div class="add-target-wrapper">
|
||||
<ha-button
|
||||
@@ -389,8 +347,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
this._pickerFilter = filter;
|
||||
};
|
||||
|
||||
private _hidePicker(ev) {
|
||||
ev.stopPropagation();
|
||||
private _hidePicker() {
|
||||
this._open = false;
|
||||
this._pickerWrapperOpen = false;
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
mdiLabel,
|
||||
mdiTextureBox,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -20,12 +19,9 @@ import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
import { getConfigEntry } from "../../data/config_entries";
|
||||
import { labelsContext } from "../../data/context";
|
||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
|
||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import type { LabelRegistryEntry } from "../../data/label_registry";
|
||||
import {
|
||||
@@ -115,10 +111,10 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const { name, context, iconPath, fallbackIconPath, stateObject, notFound } =
|
||||
const { name, context, iconPath, fallbackIconPath, stateObject } =
|
||||
this._itemData(this.type, this.itemId);
|
||||
|
||||
const showEntities = this.type !== "entity" && !notFound;
|
||||
const showEntities = this.type !== "entity";
|
||||
|
||||
const entries = this.parentEntries || this._entries;
|
||||
|
||||
@@ -132,7 +128,7 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-md-list-item type="text" class=${notFound ? "error" : ""}>
|
||||
<ha-md-list-item type="text">
|
||||
<div class="icon" slot="start">
|
||||
${this.subEntry
|
||||
? html`
|
||||
@@ -152,15 +148,11 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
/>`
|
||||
: fallbackIconPath
|
||||
? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
|
||||
: this.type === "entity"
|
||||
: stateObject
|
||||
? html`
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObject ||
|
||||
({
|
||||
entity_id: this.itemId,
|
||||
attributes: {},
|
||||
} as HassEntity)}
|
||||
.stateObj=${stateObject}
|
||||
>
|
||||
</ha-state-icon>
|
||||
`
|
||||
@@ -168,14 +160,8 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
</div>
|
||||
|
||||
<div slot="headline">${name}</div>
|
||||
${notFound || (context && !this.hideContext)
|
||||
? html`<span slot="supporting-text"
|
||||
>${notFound
|
||||
? this.hass.localize(
|
||||
`ui.components.target-picker.${this.type}_not_found`
|
||||
)
|
||||
: context}</span
|
||||
>`
|
||||
${context && !this.hideContext
|
||||
? html`<span slot="supporting-text">${context}</span>`
|
||||
: nothing}
|
||||
${this._domainName && this.subEntry
|
||||
? html`<span slot="supporting-text" class="domain"
|
||||
@@ -488,28 +474,26 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
|
||||
private _itemData = memoizeOne((type: TargetType, item: string) => {
|
||||
if (type === "floor") {
|
||||
const floor: FloorRegistryEntry | undefined = this.hass.floors?.[item];
|
||||
const floor = this.hass.floors?.[item];
|
||||
return {
|
||||
name: floor?.name || item,
|
||||
iconPath: floor?.icon,
|
||||
fallbackIconPath: floor ? floorDefaultIconPath(floor) : mdiHome,
|
||||
notFound: !floor,
|
||||
};
|
||||
}
|
||||
if (type === "area") {
|
||||
const area: AreaRegistryEntry | undefined = this.hass.areas?.[item];
|
||||
const area = this.hass.areas?.[item];
|
||||
return {
|
||||
name: area?.name || item,
|
||||
context: area?.floor_id && this.hass.floors?.[area.floor_id]?.name,
|
||||
context: area.floor_id && this.hass.floors?.[area.floor_id]?.name,
|
||||
iconPath: area?.icon,
|
||||
fallbackIconPath: mdiTextureBox,
|
||||
notFound: !area,
|
||||
};
|
||||
}
|
||||
if (type === "device") {
|
||||
const device: DeviceRegistryEntry | undefined = this.hass.devices?.[item];
|
||||
const device = this.hass.devices?.[item];
|
||||
|
||||
if (device?.primary_config_entry) {
|
||||
if (device.primary_config_entry) {
|
||||
this._getDeviceDomain(device.primary_config_entry);
|
||||
}
|
||||
|
||||
@@ -517,25 +501,24 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
name: device ? computeDeviceNameDisplay(device, this.hass) : item,
|
||||
context: device?.area_id && this.hass.areas?.[device.area_id]?.name,
|
||||
fallbackIconPath: mdiDevices,
|
||||
notFound: !device,
|
||||
};
|
||||
}
|
||||
if (type === "entity") {
|
||||
this._setDomainName(computeDomain(item));
|
||||
|
||||
const stateObject: HassEntity | undefined = this.hass.states[item];
|
||||
const entityName = stateObject
|
||||
? computeEntityName(stateObject, this.hass.entities, this.hass.devices)
|
||||
: item;
|
||||
const { area, device } = stateObject
|
||||
? getEntityContext(
|
||||
stateObject,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
)
|
||||
: { area: undefined, device: undefined };
|
||||
const stateObject = this.hass.states[item];
|
||||
const entityName = computeEntityName(
|
||||
stateObject,
|
||||
this.hass.entities,
|
||||
this.hass.devices
|
||||
);
|
||||
const { area, device } = getEntityContext(
|
||||
stateObject,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
const context = [areaName, entityName ? deviceName : undefined]
|
||||
@@ -545,19 +528,15 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
name: entityName || deviceName || item,
|
||||
context,
|
||||
stateObject,
|
||||
notFound: !stateObject,
|
||||
};
|
||||
}
|
||||
|
||||
// type label
|
||||
const label: LabelRegistryEntry | undefined = this._labelRegistry.find(
|
||||
(lab) => lab.label_id === item
|
||||
);
|
||||
const label = this._labelRegistry.find((lab) => lab.label_id === item);
|
||||
return {
|
||||
name: label?.name || item,
|
||||
iconPath: label?.icon,
|
||||
fallbackIconPath: mdiLabel,
|
||||
notFound: !label,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -618,27 +597,17 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||
}
|
||||
|
||||
.error {
|
||||
background: var(--ha-color-fill-warning-quiet-resting);
|
||||
}
|
||||
|
||||
.error [slot="supporting-text"] {
|
||||
color: var(--ha-color-on-warning-normal);
|
||||
}
|
||||
|
||||
state-badge {
|
||||
color: var(--ha-color-on-neutral-quiet);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 24px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
z-index: 1;
|
||||
}
|
||||
ha-icon-button {
|
||||
--mdc-icon-button-size: 32px;
|
||||
|
||||
@@ -705,7 +705,7 @@ export class HaTargetPickerSelector extends LitElement {
|
||||
) as EntityComboBoxItem[];
|
||||
}
|
||||
|
||||
if (!filterType && entities.length) {
|
||||
if (!filterType) {
|
||||
// show group title
|
||||
items.push(
|
||||
this.hass.localize("ui.components.target-picker.type.entities")
|
||||
@@ -733,7 +733,7 @@ export class HaTargetPickerSelector extends LitElement {
|
||||
devices = this._filterGroup("device", devices);
|
||||
}
|
||||
|
||||
if (!filterType && devices.length) {
|
||||
if (!filterType) {
|
||||
// show group title
|
||||
items.push(
|
||||
this.hass.localize("ui.components.target-picker.type.devices")
|
||||
@@ -769,7 +769,7 @@ export class HaTargetPickerSelector extends LitElement {
|
||||
) as FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
if (!filterType && areasAndFloors.length) {
|
||||
if (!filterType) {
|
||||
// show group title
|
||||
items.push(
|
||||
this.hass.localize("ui.components.target-picker.type.areas")
|
||||
@@ -811,7 +811,7 @@ export class HaTargetPickerSelector extends LitElement {
|
||||
labels = this._filterGroup("label", labels);
|
||||
}
|
||||
|
||||
if (!filterType && labels.length) {
|
||||
if (!filterType) {
|
||||
// show group title
|
||||
items.push(
|
||||
this.hass.localize("ui.components.target-picker.type.labels")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
mdiChatSleep,
|
||||
mdiClipboardList,
|
||||
mdiClock,
|
||||
mdiCodeBraces,
|
||||
mdiCog,
|
||||
mdiCommentAlert,
|
||||
mdiCounter,
|
||||
@@ -114,7 +113,6 @@ export const FALLBACK_DOMAIN_ICONS = {
|
||||
text: mdiFormTextbox,
|
||||
time: mdiClock,
|
||||
timer: mdiTimerOutline,
|
||||
template: mdiCodeBraces,
|
||||
todo: mdiClipboardList,
|
||||
tts: mdiSpeakerMessage,
|
||||
vacuum: mdiRobotVacuum,
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-icon";
|
||||
import "../../components/ha-list-item";
|
||||
import "../../components/ha-spinner";
|
||||
import type {
|
||||
ExternalEntityAddToActions,
|
||||
ExternalEntityAddToAction,
|
||||
} from "../../external_app/external_messaging";
|
||||
import { showToast } from "../../util/toast";
|
||||
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-more-info-add-to")
|
||||
export class HaMoreInfoAddTo extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entityId!: string;
|
||||
|
||||
@state() private _externalActions?: ExternalEntityAddToActions = {
|
||||
actions: [],
|
||||
};
|
||||
|
||||
@state() private _loading = true;
|
||||
|
||||
private async _loadExternalActions() {
|
||||
if (this.hass.auth.external?.config.hasEntityAddTo) {
|
||||
this._externalActions =
|
||||
await this.hass.auth.external?.sendMessage<"entity/add_to/get_actions">(
|
||||
{
|
||||
type: "entity/add_to/get_actions",
|
||||
payload: { entity_id: this.entityId },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async _actionSelected(ev: CustomEvent) {
|
||||
const action = (ev.currentTarget as any)
|
||||
.action as ExternalEntityAddToAction;
|
||||
if (!action.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.hass.auth.external!.fireMessage({
|
||||
type: "entity/add_to",
|
||||
payload: {
|
||||
entity_id: this.entityId,
|
||||
app_payload: action.app_payload,
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_failed",
|
||||
{
|
||||
error: err.message || err,
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected async firstUpdated() {
|
||||
await this._loadExternalActions();
|
||||
this._loading = false;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this._loading) {
|
||||
return html`
|
||||
<div class="loading">
|
||||
<ha-spinner></ha-spinner>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (!this._externalActions?.actions.length) {
|
||||
return html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.no_actions"
|
||||
)}
|
||||
</ha-alert>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="actions-list">
|
||||
${this._externalActions.actions.map(
|
||||
(action) => html`
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
.disabled=${!action.enabled}
|
||||
.action=${action}
|
||||
.twoline=${!!action.details}
|
||||
@click=${this._actionSelected}
|
||||
>
|
||||
<span>${action.name}</span>
|
||||
${action.details
|
||||
? html`<span slot="secondary">${action.details}</span>`
|
||||
: nothing}
|
||||
<ha-icon slot="graphic" .icon=${action.mdi_icon}></ha-icon>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-6)
|
||||
var(--ha-space-6);
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: var(--ha-space-8);
|
||||
}
|
||||
|
||||
.actions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
ha-list-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ha-list-item[disabled] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
ha-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-add-to": HaMoreInfoAddTo;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
mdiPencil,
|
||||
mdiPencilOff,
|
||||
mdiPencilOutline,
|
||||
mdiPlusBoxMultipleOutline,
|
||||
mdiTransitConnectionVariant,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
@@ -61,7 +60,6 @@ import {
|
||||
computeShowLogBookComponent,
|
||||
} from "./const";
|
||||
import "./controls/more-info-default";
|
||||
import "./ha-more-info-add-to";
|
||||
import "./ha-more-info-history-and-logbook";
|
||||
import "./ha-more-info-info";
|
||||
import "./ha-more-info-settings";
|
||||
@@ -75,7 +73,7 @@ export interface MoreInfoDialogParams {
|
||||
data?: Record<string, any>;
|
||||
}
|
||||
|
||||
type View = "info" | "history" | "settings" | "related" | "add_to";
|
||||
type View = "info" | "history" | "settings" | "related";
|
||||
|
||||
interface ChildView {
|
||||
viewTag: string;
|
||||
@@ -196,10 +194,6 @@ export class MoreInfoDialog extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _shouldShowAddEntityTo(): boolean {
|
||||
return !!this.hass.auth.external?.config.hasEntityAddTo;
|
||||
}
|
||||
|
||||
private _getDeviceId(): string | null {
|
||||
const entity = this.hass.entities[this._entityId!] as
|
||||
| EntityRegistryEntry
|
||||
@@ -301,11 +295,6 @@ export class MoreInfoDialog extends LitElement {
|
||||
this._setView("related");
|
||||
}
|
||||
|
||||
private _goToAddEntityTo(ev) {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) return;
|
||||
this._setView("add_to");
|
||||
}
|
||||
|
||||
private _breadcrumbClick(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._setView("related");
|
||||
@@ -532,22 +521,6 @@ export class MoreInfoDialog extends LitElement {
|
||||
.path=${mdiInformationOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
${this._shouldShowAddEntityTo()
|
||||
? html`
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
@request-selected=${this._goToAddEntityTo}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_entity_to"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlusBoxMultipleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
</ha-button-menu>
|
||||
`
|
||||
: nothing}
|
||||
@@ -640,14 +613,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
: "entity"}
|
||||
></ha-related-items>
|
||||
`
|
||||
: this._currView === "add_to"
|
||||
? html`
|
||||
<ha-more-info-add-to
|
||||
.hass=${this.hass}
|
||||
.entityId=${entityId}
|
||||
></ha-more-info-add-to>
|
||||
`
|
||||
: nothing
|
||||
: nothing
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -36,13 +36,6 @@ interface EMOutgoingMessageConfigGet extends EMMessage {
|
||||
type: "config/get";
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageEntityAddToGetActions extends EMMessage {
|
||||
type: "entity/add_to/get_actions";
|
||||
payload: {
|
||||
entity_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageBarCodeScan extends EMMessage {
|
||||
type: "bar_code/scan";
|
||||
payload: {
|
||||
@@ -82,10 +75,6 @@ interface EMOutgoingMessageWithAnswer {
|
||||
request: EMOutgoingMessageConfigGet;
|
||||
response: ExternalConfig;
|
||||
};
|
||||
"entity/add_to/get_actions": {
|
||||
request: EMOutgoingMessageEntityAddToGetActions;
|
||||
response: ExternalEntityAddToActions;
|
||||
};
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageExoplayerPlayHLS extends EMMessage {
|
||||
@@ -168,14 +157,6 @@ interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage {
|
||||
};
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageAddEntityTo extends EMMessage {
|
||||
type: "entity/add_to";
|
||||
payload: {
|
||||
entity_id: string;
|
||||
app_payload: string; // Opaque string received from get_actions
|
||||
};
|
||||
}
|
||||
|
||||
type EMOutgoingMessageWithoutAnswer =
|
||||
| EMMessageResultError
|
||||
| EMMessageResultSuccess
|
||||
@@ -196,8 +177,7 @@ type EMOutgoingMessageWithoutAnswer =
|
||||
| EMOutgoingMessageThemeUpdate
|
||||
| EMOutgoingMessageThreadStoreInPlatformKeychain
|
||||
| EMOutgoingMessageImprovScan
|
||||
| EMOutgoingMessageImprovConfigureDevice
|
||||
| EMOutgoingMessageAddEntityTo;
|
||||
| EMOutgoingMessageImprovConfigureDevice;
|
||||
|
||||
export interface EMIncomingMessageRestart {
|
||||
id: number;
|
||||
@@ -313,31 +293,18 @@ type EMIncomingMessage =
|
||||
type EMIncomingMessageHandler = (msg: EMIncomingMessageCommands) => boolean;
|
||||
|
||||
export interface ExternalConfig {
|
||||
hasSettingsScreen?: boolean;
|
||||
hasSidebar?: boolean;
|
||||
canWriteTag?: boolean;
|
||||
hasExoPlayer?: boolean;
|
||||
canCommissionMatter?: boolean;
|
||||
canImportThreadCredentials?: boolean;
|
||||
canTransferThreadCredentialsToKeychain?: boolean;
|
||||
hasAssist?: boolean;
|
||||
hasBarCodeScanner?: number;
|
||||
canSetupImprov?: boolean;
|
||||
downloadFileSupported?: boolean;
|
||||
appVersion?: string;
|
||||
hasEntityAddTo?: boolean; // Supports "Add to" from more-info dialog, with action coming from external app
|
||||
}
|
||||
|
||||
export interface ExternalEntityAddToAction {
|
||||
enabled: boolean;
|
||||
name: string; // Translated name of the action to be displayed in the UI
|
||||
details?: string; // Optional translated details of the action to be displayed in the UI
|
||||
mdi_icon: string; // MDI icon name to be displayed in the UI (e.g., "mdi:car")
|
||||
app_payload: string; // Opaque string to be sent back when the action is selected
|
||||
}
|
||||
|
||||
export interface ExternalEntityAddToActions {
|
||||
actions: ExternalEntityAddToAction[];
|
||||
hasSettingsScreen: boolean;
|
||||
hasSidebar: boolean;
|
||||
canWriteTag: boolean;
|
||||
hasExoPlayer: boolean;
|
||||
canCommissionMatter: boolean;
|
||||
canImportThreadCredentials: boolean;
|
||||
canTransferThreadCredentialsToKeychain: boolean;
|
||||
hasAssist: boolean;
|
||||
hasBarCodeScanner: number;
|
||||
canSetupImprov: boolean;
|
||||
downloadFileSupported: boolean;
|
||||
appVersion: string;
|
||||
}
|
||||
|
||||
export class ExternalMessaging {
|
||||
|
||||
@@ -20,21 +20,6 @@
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
<%= renderTemplate("_style_base.html.template") %>
|
||||
<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 {
|
||||
background-color: var(--primary-background-color, #fafafa);
|
||||
color: var(--primary-text-color, #212121);
|
||||
@@ -47,28 +32,11 @@
|
||||
}
|
||||
}
|
||||
#ha-launch-screen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
view-transition-name: launch-screen;
|
||||
background-color: var(--primary-background-color, #fafafa);
|
||||
z-index: 100;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#ha-launch-screen {
|
||||
background-color: var(--primary-background-color, #111111);
|
||||
}
|
||||
}
|
||||
#ha-launch-screen.removing {
|
||||
opacity: 0;
|
||||
}
|
||||
#ha-launch-screen svg {
|
||||
width: 112px;
|
||||
|
||||
@@ -33,7 +33,7 @@ const COMPONENTS = {
|
||||
"media-browser": () =>
|
||||
import("../panels/media-browser/ha-panel-media-browser"),
|
||||
light: () => import("../panels/light/ha-panel-light"),
|
||||
security: () => import("../panels/security/ha-panel-security"),
|
||||
safety: () => import("../panels/safety/ha-panel-safety"),
|
||||
climate: () => import("../panels/climate/ha-panel-climate"),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { goBack } from "../../common/navigate";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/ha-icon-button-arrow-prev";
|
||||
import "../../components/ha-menu-button";
|
||||
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
|
||||
import type { LovelaceConfig } from "../../data/lovelace/config/types";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
|
||||
import type { Lovelace } from "../lovelace/types";
|
||||
import "../lovelace/views/hui-view";
|
||||
import "../lovelace/views/hui-view-container";
|
||||
|
||||
const CLIMATE_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
|
||||
strategy: {
|
||||
type: "climate",
|
||||
},
|
||||
const CLIMATE_LOVELACE_CONFIG: LovelaceConfig = {
|
||||
views: [
|
||||
{
|
||||
strategy: {
|
||||
type: "climate",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@customElement("ha-panel-climate")
|
||||
@@ -32,119 +33,65 @@ class PanelClimate extends LitElement {
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
public firstUpdated(_changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
}
|
||||
|
||||
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) {
|
||||
if (oldHass?.locale !== this.hass.locale) {
|
||||
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();
|
||||
};
|
||||
|
||||
private _back(ev) {
|
||||
ev.stopPropagation();
|
||||
goBack();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="header">
|
||||
<div class="toolbar">
|
||||
${
|
||||
this._searchParms.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`
|
||||
}
|
||||
${this._searchParms.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`}
|
||||
<div class="main-title">${this.hass.localize("panel.climate")}</div>
|
||||
</div>
|
||||
</div>
|
||||
${
|
||||
this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view>
|
||||
</hui-view-container>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _setLovelace() {
|
||||
const viewConfig = await generateLovelaceViewStrategy(
|
||||
CLIMATE_LOVELACE_VIEW_CONFIG,
|
||||
this.hass
|
||||
);
|
||||
|
||||
const config = { views: [viewConfig] };
|
||||
const rawConfig = { views: [CLIMATE_LOVELACE_VIEW_CONFIG] };
|
||||
|
||||
if (deepEqual(config, this._lovelace?.config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
private _setLovelace() {
|
||||
this._lovelace = {
|
||||
config: config,
|
||||
rawConfig: rawConfig,
|
||||
config: CLIMATE_LOVELACE_CONFIG,
|
||||
rawConfig: CLIMATE_LOVELACE_CONFIG,
|
||||
editMode: false,
|
||||
urlPath: "climate",
|
||||
mode: "generated",
|
||||
|
||||
@@ -257,7 +257,7 @@ class DialogAddAutomationElement
|
||||
|
||||
const results = fuse.multiTermsSearch(filter);
|
||||
if (results) {
|
||||
return results.map((result) => result.item).filter((item) => item.name);
|
||||
return results.map((result) => result.item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
@@ -294,7 +294,7 @@ class DialogAddAutomationElement
|
||||
|
||||
const results = fuse.multiTermsSearch(filter);
|
||||
if (results) {
|
||||
return results.map((result) => result.item).filter((item) => item.name);
|
||||
return results.map((result) => result.item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
@@ -383,16 +383,9 @@ class DialogAddAutomationElement
|
||||
|
||||
generatedCollections.push({
|
||||
titleKey: collection.titleKey,
|
||||
groups: groups.sort((a, b) => {
|
||||
// make sure device is always on top
|
||||
if (a.key === "device" || a.key === "device_id") {
|
||||
return -1;
|
||||
}
|
||||
if (b.key === "device" || b.key === "device_id") {
|
||||
return 1;
|
||||
}
|
||||
return stringCompare(a.name, b.name, this.hass.locale.language);
|
||||
}),
|
||||
groups: groups.sort((a, b) =>
|
||||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||||
),
|
||||
});
|
||||
});
|
||||
return generatedCollections;
|
||||
@@ -685,7 +678,7 @@ class DialogAddAutomationElement
|
||||
);
|
||||
|
||||
const typeTitle = this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${automationElementType}s.add`
|
||||
`ui.panel.config.automation.editor.${automationElementType}s.header`
|
||||
);
|
||||
|
||||
const tabButtons = [
|
||||
@@ -902,9 +895,7 @@ class DialogAddAutomationElement
|
||||
|
||||
return html`
|
||||
<div class="items-title ${this._itemsScrolled ? "scrolled" : ""}">
|
||||
${this._tab === "blocks" && !this._filter
|
||||
? this.hass.localize("ui.panel.config.automation.editor.blocks")
|
||||
: title}
|
||||
${title}
|
||||
</div>
|
||||
<ha-md-list
|
||||
dialogInitialFocus=${ifDefined(this._fullScreen ? "" : undefined)}
|
||||
|
||||
@@ -9,7 +9,6 @@ import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-radio";
|
||||
import "../../../../components/ha-markdown";
|
||||
import type { HaRadio } from "../../../../components/ha-radio";
|
||||
import type {
|
||||
FlowFromGridSourceEnergyPreference,
|
||||
@@ -20,7 +19,11 @@ import {
|
||||
emptyFlowToGridSourceEnergyPreference,
|
||||
energyStatisticHelpUrl,
|
||||
} from "../../../../data/energy";
|
||||
import { isExternalStatistic } from "../../../../data/recorder";
|
||||
import {
|
||||
getDisplayUnit,
|
||||
getStatisticMetadata,
|
||||
isExternalStatistic,
|
||||
} from "../../../../data/recorder";
|
||||
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
@@ -44,6 +47,8 @@ export class DialogEnergyGridFlowSettings
|
||||
|
||||
@state() private _costs?: "no-costs" | "number" | "entity" | "statistic";
|
||||
|
||||
@state() private _pickedDisplayUnit?: string | null;
|
||||
|
||||
@state() private _energy_units?: string[];
|
||||
|
||||
@state() private _error?: string;
|
||||
@@ -76,6 +81,11 @@ export class DialogEnergyGridFlowSettings
|
||||
: "stat_energy_to"
|
||||
];
|
||||
|
||||
this._pickedDisplayUnit = getDisplayUnit(
|
||||
this.hass,
|
||||
initialSourceId,
|
||||
params.metadata
|
||||
);
|
||||
this._energy_units = (
|
||||
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
|
||||
).units;
|
||||
@@ -93,6 +103,7 @@ export class DialogEnergyGridFlowSettings
|
||||
public closeDialog() {
|
||||
this._params = undefined;
|
||||
this._source = undefined;
|
||||
this._pickedDisplayUnit = undefined;
|
||||
this._error = undefined;
|
||||
this._excludeList = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
@@ -106,6 +117,10 @@ export class DialogEnergyGridFlowSettings
|
||||
|
||||
const pickableUnit = this._energy_units?.join(", ") || "";
|
||||
|
||||
const unitPriceSensor = this._pickedDisplayUnit
|
||||
? `${this.hass.config.currency}/${this._pickedDisplayUnit}`
|
||||
: undefined;
|
||||
|
||||
const unitPriceFixed = `${this.hass.config.currency}/kWh`;
|
||||
|
||||
const externalSource =
|
||||
@@ -231,15 +246,9 @@ export class DialogEnergyGridFlowSettings
|
||||
.hass=${this.hass}
|
||||
include-domains='["sensor", "input_number"]'
|
||||
.value=${this._source.entity_energy_price}
|
||||
.label=${this.hass.localize(
|
||||
.label=${`${this.hass.localize(
|
||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.cost_entity_input`
|
||||
)}
|
||||
.helper=${html`<ha-markdown
|
||||
.content=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.flow_dialog.cost_entity_helper",
|
||||
{ currency: this.hass.config.currency }
|
||||
)}
|
||||
></ha-markdown>`}
|
||||
)} ${unitPriceSensor ? ` (${unitPriceSensor})` : ""}`}
|
||||
@value-changed=${this._priceEntityChanged}
|
||||
></ha-entity-picker>`
|
||||
: ""}
|
||||
@@ -332,6 +341,16 @@ export class DialogEnergyGridFlowSettings
|
||||
}
|
||||
|
||||
private async _statisticChanged(ev: CustomEvent<{ value: string }>) {
|
||||
if (ev.detail.value) {
|
||||
const metadata = await getStatisticMetadata(this.hass, [ev.detail.value]);
|
||||
this._pickedDisplayUnit = getDisplayUnit(
|
||||
this.hass,
|
||||
ev.detail.value,
|
||||
metadata[0]
|
||||
);
|
||||
} else {
|
||||
this._pickedDisplayUnit = undefined;
|
||||
}
|
||||
this._source = {
|
||||
...this._source!,
|
||||
[this._params!.direction === "from"
|
||||
|
||||
@@ -332,13 +332,13 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.hass.panels.security) {
|
||||
if (this.hass.panels.safety) {
|
||||
result.push({
|
||||
icon: "mdi:security",
|
||||
title: this.hass.localize("panel.security"),
|
||||
title: this.hass.localize("panel.safety"),
|
||||
show_in_sidebar: false,
|
||||
mode: "storage",
|
||||
url_path: "security",
|
||||
url_path: "safety",
|
||||
filename: "",
|
||||
default: false,
|
||||
require_admin: false,
|
||||
@@ -470,13 +470,13 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
}
|
||||
|
||||
private _canDelete(urlPath: string) {
|
||||
return !["lovelace", "energy", "light", "security", "climate"].includes(
|
||||
return !["lovelace", "energy", "light", "safety", "climate"].includes(
|
||||
urlPath
|
||||
);
|
||||
}
|
||||
|
||||
private _canEdit(urlPath: string) {
|
||||
return !["light", "security", "climate"].includes(urlPath);
|
||||
return !["light", "safety", "climate"].includes(urlPath);
|
||||
}
|
||||
|
||||
private _handleDelete = async (item: DataTableItem) => {
|
||||
|
||||
@@ -109,26 +109,15 @@ export class AssistPipelineDetailConversation extends LitElement {
|
||||
}
|
||||
|
||||
private _supportedLanguagesChanged(ev) {
|
||||
this._supportedLanguages = ev.detail.value;
|
||||
|
||||
if (
|
||||
this._supportedLanguages === "*" ||
|
||||
!this._supportedLanguages?.includes(
|
||||
this.data?.conversation_language || ""
|
||||
) ||
|
||||
!this.data?.conversation_language
|
||||
) {
|
||||
if (ev.detail.value === "*") {
|
||||
// wait for update of conversation_engine
|
||||
setTimeout(() => {
|
||||
const value = { ...this.data };
|
||||
if (this._supportedLanguages === "*") {
|
||||
value.conversation_language = "*";
|
||||
} else {
|
||||
value.conversation_language = this._supportedLanguages?.[0] ?? null;
|
||||
}
|
||||
value.conversation_language = "*";
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}, 0);
|
||||
}
|
||||
this._supportedLanguages = ev.detail.value;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
|
||||
@@ -214,7 +214,7 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement {
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updatePipeline}
|
||||
.loading=${this._submitting}
|
||||
.disabled=${this._submitting}
|
||||
dialogInitialFocus
|
||||
>
|
||||
${this._params.pipeline?.id
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { goBack } from "../../common/navigate";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/ha-icon-button-arrow-prev";
|
||||
import "../../components/ha-menu-button";
|
||||
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
|
||||
import type { LovelaceConfig } from "../../data/lovelace/config/types";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
|
||||
import type { Lovelace } from "../lovelace/types";
|
||||
import "../lovelace/views/hui-view";
|
||||
import "../lovelace/views/hui-view-container";
|
||||
|
||||
const LIGHT_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
|
||||
strategy: {
|
||||
type: "light",
|
||||
},
|
||||
const LIGHT_LOVELACE_CONFIG: LovelaceConfig = {
|
||||
views: [
|
||||
{
|
||||
strategy: {
|
||||
type: "light",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@customElement("ha-panel-light")
|
||||
@@ -33,118 +34,60 @@ class PanelLight extends LitElement {
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
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) {
|
||||
if (oldHass?.locale !== this.hass.locale) {
|
||||
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();
|
||||
};
|
||||
|
||||
private _back(ev) {
|
||||
ev.stopPropagation();
|
||||
goBack();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="header">
|
||||
<div class="toolbar">
|
||||
${
|
||||
this._searchParms.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`
|
||||
}
|
||||
${this._searchParms.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`}
|
||||
<div class="main-title">${this.hass.localize("panel.light")}</div>
|
||||
</div>
|
||||
</div>
|
||||
${
|
||||
this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view>
|
||||
</hui-view-container>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _setLovelace() {
|
||||
const viewConfig = await generateLovelaceViewStrategy(
|
||||
LIGHT_LOVELACE_VIEW_CONFIG,
|
||||
this.hass
|
||||
);
|
||||
|
||||
const config = { views: [viewConfig] };
|
||||
const rawConfig = { views: [LIGHT_LOVELACE_VIEW_CONFIG] };
|
||||
|
||||
if (deepEqual(config, this._lovelace?.config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
private _setLovelace() {
|
||||
this._lovelace = {
|
||||
config: config,
|
||||
rawConfig: rawConfig,
|
||||
config: LIGHT_LOVELACE_CONFIG,
|
||||
rawConfig: LIGHT_LOVELACE_CONFIG,
|
||||
editMode: false,
|
||||
urlPath: "light",
|
||||
mode: "generated",
|
||||
|
||||
@@ -33,7 +33,7 @@ import type { HomeSummaryCard } from "./types";
|
||||
const COLORS: Record<HomeSummary, string> = {
|
||||
light: "amber",
|
||||
climate: "deep-orange",
|
||||
security: "blue-grey",
|
||||
safety: "blue-grey",
|
||||
media_players: "blue",
|
||||
};
|
||||
|
||||
@@ -142,20 +142,20 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
|
||||
? `${formattedMinTemp}°`
|
||||
: `${formattedMinTemp} - ${formattedMaxTemp}°`;
|
||||
}
|
||||
case "security": {
|
||||
case "safety": {
|
||||
// Alarm and lock status
|
||||
const securityFilters = HOME_SUMMARIES_FILTERS.security.map((filter) =>
|
||||
const safetyFilters = HOME_SUMMARIES_FILTERS.safety.map((filter) =>
|
||||
generateEntityFilter(this.hass!, filter)
|
||||
);
|
||||
|
||||
const securityEntities = findEntities(allEntities, securityFilters);
|
||||
const safetyEntities = findEntities(allEntities, safetyFilters);
|
||||
|
||||
const locks = securityEntities.filter((entityId) => {
|
||||
const locks = safetyEntities.filter((entityId) => {
|
||||
const domain = computeDomain(entityId);
|
||||
return domain === "lock";
|
||||
});
|
||||
|
||||
const alarms = securityEntities.filter((entityId) => {
|
||||
const alarms = safetyEntities.filter((entityId) => {
|
||||
const domain = computeDomain(entityId);
|
||||
return domain === "alarm_control_panel";
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
|
||||
@@ -98,10 +97,7 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
|
||||
: `${sandbox_user_params} ${IFRAME_SANDBOX}`;
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
class=${classMap({ "hide-background": this._config.hide_background })}
|
||||
.header=${this._config.title}
|
||||
>
|
||||
<ha-card .header=${this._config.title}>
|
||||
<div
|
||||
id="root"
|
||||
style=${styleMap({
|
||||
@@ -137,12 +133,6 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
ha-card.hide-background {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -323,7 +323,6 @@ export interface IframeCardConfig extends LovelaceCardConfig {
|
||||
title?: string;
|
||||
allow?: string;
|
||||
url: string;
|
||||
hide_background?: boolean;
|
||||
}
|
||||
|
||||
export interface LightCardConfig extends LovelaceCardConfig {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ const cardConfigStruct = assign(
|
||||
url: optional(string()),
|
||||
aspect_ratio: optional(string()),
|
||||
allow_open_top_navigation: optional(boolean()),
|
||||
hide_background: optional(boolean()),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -30,7 +29,6 @@ const SCHEMA = [
|
||||
{ name: "aspect_ratio", selector: { text: {} } },
|
||||
],
|
||||
},
|
||||
{ name: "hide_background", selector: { boolean: {} } },
|
||||
] as const;
|
||||
|
||||
@customElement("hui-iframe-card-editor")
|
||||
@@ -58,7 +56,6 @@ export class HuiIframeCardEditor
|
||||
.data=${this._config}
|
||||
.schema=${SCHEMA}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.computeHelper=${this._computeHelperCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
@@ -68,29 +65,8 @@ export class HuiIframeCardEditor
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
|
||||
switch (schema.name) {
|
||||
case "hide_background":
|
||||
return this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.iframe.hide_background"
|
||||
);
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private _computeHelperCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
|
||||
switch (schema.name) {
|
||||
case "hide_background":
|
||||
return this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.iframe.hide_background_helper"
|
||||
);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) =>
|
||||
this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`);
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
array,
|
||||
assert,
|
||||
@@ -11,21 +9,23 @@ import {
|
||||
optional,
|
||||
string,
|
||||
} from "superstruct";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/entity/ha-entities-picker";
|
||||
import "../../../../components/ha-target-picker";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import "../../../../components/ha-target-picker";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../../../data/entity";
|
||||
import { filterLogbookCompatibleEntities } from "../../../../data/logbook";
|
||||
import { targetStruct } from "../../../../data/script";
|
||||
import { resolveEntityIDs } from "../../../../data/selector";
|
||||
import { getSensorNumericDeviceClasses } from "../../../../data/sensor";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { DEFAULT_HOURS_TO_SHOW } from "../../cards/hui-logbook-card";
|
||||
import type { LogbookCardConfig } from "../../cards/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { DEFAULT_HOURS_TO_SHOW } from "../../cards/hui-logbook-card";
|
||||
import { targetStruct } from "../../../../data/script";
|
||||
import { getSensorNumericDeviceClasses } from "../../../../data/sensor";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../../../data/entity";
|
||||
import { resolveEntityIDs } from "../../../../data/selector";
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
@@ -132,6 +132,7 @@ export class HuiLogbookCardEditor
|
||||
.hass=${this.hass}
|
||||
.entityFilter=${this._filterFunc}
|
||||
.value=${this._targetPicker}
|
||||
add-on-top
|
||||
@value-changed=${this._entitiesChanged}
|
||||
></ha-target-picker>
|
||||
`;
|
||||
@@ -188,13 +189,6 @@ export class HuiLogbookCardEditor
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
ha-target-picker {
|
||||
display: block;
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -48,7 +48,7 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
|
||||
import("./home/home-media-players-view-strategy"),
|
||||
"home-area": () => import("./home/home-area-view-strategy"),
|
||||
light: () => import("../../light/strategies/light-view-strategy"),
|
||||
security: () => import("../../security/strategies/security-view-strategy"),
|
||||
safety: () => import("../../safety/strategies/safety-view-strategy"),
|
||||
climate: () => import("../../climate/strategies/climate-view-strategy"),
|
||||
},
|
||||
section: {
|
||||
|
||||
@@ -2,12 +2,12 @@ import type { EntityFilter } from "../../../../../common/entity/entity_filter";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import { climateEntityFilters } from "../../../../climate/strategies/climate-view-strategy";
|
||||
import { lightEntityFilters } from "../../../../light/strategies/light-view-strategy";
|
||||
import { securityEntityFilters } from "../../../../security/strategies/security-view-strategy";
|
||||
import { safetyEntityFilters } from "../../../../safety/strategies/safety-view-strategy";
|
||||
|
||||
export const HOME_SUMMARIES = [
|
||||
"light",
|
||||
"climate",
|
||||
"security",
|
||||
"safety",
|
||||
"media_players",
|
||||
] as const;
|
||||
|
||||
@@ -16,14 +16,14 @@ export type HomeSummary = (typeof HOME_SUMMARIES)[number];
|
||||
export const HOME_SUMMARIES_ICONS: Record<HomeSummary, string> = {
|
||||
light: "mdi:lamps",
|
||||
climate: "mdi:home-thermometer",
|
||||
security: "mdi:security",
|
||||
safety: "mdi:security",
|
||||
media_players: "mdi:multimedia",
|
||||
};
|
||||
|
||||
export const HOME_SUMMARIES_FILTERS: Record<HomeSummary, EntityFilter[]> = {
|
||||
light: lightEntityFilters,
|
||||
climate: climateEntityFilters,
|
||||
security: securityEntityFilters,
|
||||
safety: safetyEntityFilters,
|
||||
media_players: [{ domain: "media_player", entity_category: "none" }],
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@ export const getSummaryLabel = (
|
||||
localize: LocalizeFunc,
|
||||
summary: HomeSummary
|
||||
) => {
|
||||
if (summary === "light" || summary === "climate" || summary === "security") {
|
||||
if (summary === "light" || summary === "climate" || summary === "safety") {
|
||||
return localize(`panel.${summary}`);
|
||||
}
|
||||
return localize(`ui.panel.lovelace.strategy.home.summary_list.${summary}`);
|
||||
|
||||
@@ -104,7 +104,7 @@ export class HomeAreaViewStrategy extends ReactiveElement {
|
||||
const {
|
||||
light,
|
||||
climate,
|
||||
security,
|
||||
safety,
|
||||
media_players: mediaPlayers,
|
||||
} = entitiesBySummary;
|
||||
|
||||
@@ -136,16 +136,16 @@ export class HomeAreaViewStrategy extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
if (security.length > 0) {
|
||||
if (safety.length > 0) {
|
||||
sections.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
computeHeadingCard(
|
||||
getSummaryLabel(hass.localize, "security"),
|
||||
HOME_SUMMARIES_ICONS.security,
|
||||
"/security?historyBack=1"
|
||||
getSummaryLabel(hass.localize, "safety"),
|
||||
HOME_SUMMARIES_ICONS.safety,
|
||||
"/safety?historyBack=1"
|
||||
),
|
||||
...security.map(computeTileCard),
|
||||
...safety.map(computeTileCard),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import {
|
||||
findEntities,
|
||||
generateEntityFilter,
|
||||
} from "../../../../common/entity/entity_filter";
|
||||
import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
|
||||
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
|
||||
import type { AreaRegistryEntry } from "../../../../data/area_registry";
|
||||
import { getEnergyPreferences } from "../../../../data/energy";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
@@ -25,7 +21,7 @@ import type {
|
||||
import { getAreas, getFloors } from "../areas/helpers/areas-strategy-helper";
|
||||
import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
|
||||
import { getHomeStructure } from "./helpers/home-structure";
|
||||
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
||||
import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
|
||||
|
||||
export interface HomeMainViewStrategyConfig {
|
||||
type: "home-main";
|
||||
@@ -148,105 +144,82 @@ export class HomeMainViewStrategy extends ReactiveElement {
|
||||
column_span: maxColumns,
|
||||
} as LovelaceStrategySectionConfig;
|
||||
|
||||
const allEntities = Object.keys(hass.states);
|
||||
|
||||
const mediaPlayerFilter = HOME_SUMMARIES_FILTERS.media_players.map(
|
||||
(filter) => generateEntityFilter(hass, filter)
|
||||
);
|
||||
|
||||
const lightsFilters = HOME_SUMMARIES_FILTERS.light.map((filter) =>
|
||||
generateEntityFilter(hass, filter)
|
||||
);
|
||||
|
||||
const climateFilters = HOME_SUMMARIES_FILTERS.climate.map((filter) =>
|
||||
generateEntityFilter(hass, filter)
|
||||
);
|
||||
|
||||
const securityFilters = HOME_SUMMARIES_FILTERS.security.map((filter) =>
|
||||
generateEntityFilter(hass, filter)
|
||||
);
|
||||
|
||||
const hasLights = findEntities(allEntities, lightsFilters).length > 0;
|
||||
const hasMediaPlayers =
|
||||
findEntities(allEntities, mediaPlayerFilter).length > 0;
|
||||
const hasClimate = findEntities(allEntities, climateFilters).length > 0;
|
||||
const hasSecurity = findEntities(allEntities, securityFilters).length > 0;
|
||||
// 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[] = [
|
||||
hasLights &&
|
||||
({
|
||||
type: "home-summary",
|
||||
summary: "light",
|
||||
vertical: true,
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/light?historyBack=1",
|
||||
},
|
||||
grid_options: {
|
||||
rows: 2,
|
||||
columns: 4,
|
||||
},
|
||||
} satisfies HomeSummaryCard),
|
||||
hasClimate &&
|
||||
({
|
||||
type: "home-summary",
|
||||
summary: "climate",
|
||||
vertical: true,
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/climate?historyBack=1",
|
||||
},
|
||||
grid_options: {
|
||||
rows: 2,
|
||||
columns: 4,
|
||||
},
|
||||
} satisfies HomeSummaryCard),
|
||||
hasSecurity &&
|
||||
({
|
||||
type: "home-summary",
|
||||
summary: "security",
|
||||
vertical: true,
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/security?historyBack=1",
|
||||
},
|
||||
grid_options: {
|
||||
rows: 2,
|
||||
columns: 4,
|
||||
},
|
||||
} satisfies HomeSummaryCard),
|
||||
hasMediaPlayers &&
|
||||
({
|
||||
type: "home-summary",
|
||||
summary: "media_players",
|
||||
vertical: true,
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "media-players",
|
||||
},
|
||||
grid_options: {
|
||||
rows: 2,
|
||||
columns: 4,
|
||||
},
|
||||
} satisfies HomeSummaryCard),
|
||||
].filter(Boolean) as 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: [],
|
||||
cards: summaryCards,
|
||||
};
|
||||
|
||||
if (summaryCards.length) {
|
||||
summarySection.cards!.push(
|
||||
{
|
||||
type: "heading",
|
||||
heading: hass.localize("ui.panel.lovelace.strategy.home.summaries"),
|
||||
},
|
||||
...summaryCards
|
||||
);
|
||||
}
|
||||
|
||||
const weatherFilter = generateEntityFilter(hass, {
|
||||
domain: "weather",
|
||||
entity_category: "none",
|
||||
@@ -302,7 +275,7 @@ export class HomeMainViewStrategy extends ReactiveElement {
|
||||
[
|
||||
favoriteSection.cards && favoriteSection,
|
||||
commonControlsSection,
|
||||
summarySection.cards && summarySection,
|
||||
summarySection,
|
||||
...floorsSections,
|
||||
widgetSection.cards && widgetSection,
|
||||
] satisfies (LovelaceSectionRawConfig | undefined)[]
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { goBack } from "../../common/navigate";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/ha-icon-button-arrow-prev";
|
||||
import "../../components/ha-menu-button";
|
||||
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
|
||||
import type { LovelaceConfig } from "../../data/lovelace/config/types";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
|
||||
import type { Lovelace } from "../lovelace/types";
|
||||
import "../lovelace/views/hui-view";
|
||||
import "../lovelace/views/hui-view-container";
|
||||
|
||||
const SECURITY_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
|
||||
strategy: {
|
||||
type: "security",
|
||||
},
|
||||
const SAFETY_LOVELACE_CONFIG: LovelaceConfig = {
|
||||
views: [
|
||||
{
|
||||
strategy: {
|
||||
type: "safety",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@customElement("ha-panel-security")
|
||||
class PanelSecurity extends LitElement {
|
||||
@customElement("ha-panel-safety")
|
||||
class PanelSafety extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
@@ -33,120 +34,62 @@ class PanelSecurity extends LitElement {
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
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) {
|
||||
if (oldHass?.locale !== this.hass.locale) {
|
||||
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();
|
||||
};
|
||||
|
||||
private _back(ev) {
|
||||
ev.stopPropagation();
|
||||
goBack();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="header">
|
||||
<div class="toolbar">
|
||||
${
|
||||
this._searchParms.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`
|
||||
}
|
||||
<div class="main-title">${this.hass.localize("panel.security")}</div>
|
||||
</div>
|
||||
</div>
|
||||
${
|
||||
this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view
|
||||
${this._searchParms.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
></ha-menu-button>
|
||||
`}
|
||||
<div class="main-title">${this.hass.localize("panel.safety")}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view>
|
||||
</hui-view-container>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _setLovelace() {
|
||||
const viewConfig = await generateLovelaceViewStrategy(
|
||||
SECURITY_LOVELACE_VIEW_CONFIG,
|
||||
this.hass
|
||||
);
|
||||
|
||||
const config = { views: [viewConfig] };
|
||||
const rawConfig = { views: [SECURITY_LOVELACE_VIEW_CONFIG] };
|
||||
|
||||
if (deepEqual(config, this._lovelace?.config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
private _setLovelace() {
|
||||
this._lovelace = {
|
||||
config: config,
|
||||
rawConfig: rawConfig,
|
||||
config: SAFETY_LOVELACE_CONFIG,
|
||||
rawConfig: SAFETY_LOVELACE_CONFIG,
|
||||
editMode: false,
|
||||
urlPath: "security",
|
||||
urlPath: "safety",
|
||||
mode: "generated",
|
||||
locale: this.hass.locale,
|
||||
enableFullEditMode: () => undefined,
|
||||
@@ -248,6 +191,6 @@ class PanelSecurity extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-panel-security": PanelSecurity;
|
||||
"ha-panel-safety": PanelSafety;
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,11 @@ import {
|
||||
} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure";
|
||||
|
||||
export interface SecurityViewStrategyConfig {
|
||||
type: "security";
|
||||
export interface SafetyViewStrategyConfig {
|
||||
type: "safety";
|
||||
}
|
||||
|
||||
export const securityEntityFilters: EntityFilter[] = [
|
||||
export const safetyEntityFilters: EntityFilter[] = [
|
||||
{
|
||||
domain: "camera",
|
||||
entity_category: "none",
|
||||
@@ -67,7 +67,7 @@ export const securityEntityFilters: EntityFilter[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const processAreasForSecurity = (
|
||||
const processAreasForSafety = (
|
||||
areaIds: string[],
|
||||
hass: HomeAssistant,
|
||||
entities: string[]
|
||||
@@ -81,12 +81,12 @@ const processAreasForSecurity = (
|
||||
const areaFilter = generateEntityFilter(hass, {
|
||||
area: area.area_id,
|
||||
});
|
||||
const areaSecurityEntities = entities.filter(areaFilter);
|
||||
const areaSafetyEntities = entities.filter(areaFilter);
|
||||
const areaCards: LovelaceCardConfig[] = [];
|
||||
|
||||
const computeTileCard = computeAreaTileCardConfig(hass, "", false);
|
||||
|
||||
for (const entityId of areaSecurityEntities) {
|
||||
for (const entityId of areaSafetyEntities) {
|
||||
areaCards.push(computeTileCard(entityId));
|
||||
}
|
||||
|
||||
@@ -121,10 +121,10 @@ const processUnassignedEntities = (
|
||||
return areaCards;
|
||||
};
|
||||
|
||||
@customElement("security-view-strategy")
|
||||
export class SecurityViewStrategy extends ReactiveElement {
|
||||
@customElement("safety-view-strategy")
|
||||
export class SafetyViewStrategy extends ReactiveElement {
|
||||
static async generate(
|
||||
_config: SecurityViewStrategyConfig,
|
||||
_config: SafetyViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const areas = getAreas(hass.areas);
|
||||
@@ -135,11 +135,11 @@ export class SecurityViewStrategy extends ReactiveElement {
|
||||
|
||||
const allEntities = Object.keys(hass.states);
|
||||
|
||||
const securityFilters = securityEntityFilters.map((filter) =>
|
||||
const safetyFilters = safetyEntityFilters.map((filter) =>
|
||||
generateEntityFilter(hass, filter)
|
||||
);
|
||||
|
||||
const entities = findEntities(allEntities, securityFilters);
|
||||
const entities = findEntities(allEntities, safetyFilters);
|
||||
|
||||
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
|
||||
|
||||
@@ -164,7 +164,7 @@ export class SecurityViewStrategy extends ReactiveElement {
|
||||
],
|
||||
};
|
||||
|
||||
const areaCards = processAreasForSecurity(areaIds, hass, entities);
|
||||
const areaCards = processAreasForSafety(areaIds, hass, entities);
|
||||
|
||||
if (areaCards.length > 0) {
|
||||
section.cards!.push(...areaCards);
|
||||
@@ -188,7 +188,7 @@ export class SecurityViewStrategy extends ReactiveElement {
|
||||
],
|
||||
};
|
||||
|
||||
const areaCards = processAreasForSecurity(home.areas, hass, entities);
|
||||
const areaCards = processAreasForSafety(home.areas, hass, entities);
|
||||
|
||||
if (areaCards.length > 0) {
|
||||
section.cards!.push(...areaCards);
|
||||
@@ -209,9 +209,9 @@ export class SecurityViewStrategy extends ReactiveElement {
|
||||
heading:
|
||||
sections.length > 0
|
||||
? hass.localize(
|
||||
"ui.panel.lovelace.strategy.security.other_devices"
|
||||
"ui.panel.lovelace.strategy.safety.other_devices"
|
||||
)
|
||||
: hass.localize("ui.panel.lovelace.strategy.security.devices"),
|
||||
: hass.localize("ui.panel.lovelace.strategy.safety.devices"),
|
||||
},
|
||||
...unassignedCards,
|
||||
],
|
||||
@@ -229,6 +229,6 @@ export class SecurityViewStrategy extends ReactiveElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"security-view-strategy": SecurityViewStrategy;
|
||||
"safety-view-strategy": SafetyViewStrategy;
|
||||
}
|
||||
}
|
||||
@@ -199,23 +199,3 @@ export const baseEntrypointStyles = css`
|
||||
width: 100vw;
|
||||
}
|
||||
`;
|
||||
|
||||
export const baseAnimationStyles = css`
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -42,14 +42,6 @@ export const coreStyles = css`
|
||||
--ha-space-18: 72px;
|
||||
--ha-space-19: 76px;
|
||||
--ha-space-20: 80px;
|
||||
|
||||
--ha-animation-base-duration: 350ms;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
--ha-animation-base-duration: 0ms;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"media_browser": "Media",
|
||||
"profile": "Profile",
|
||||
"light": "Lights",
|
||||
"security": "Security",
|
||||
"safety": "Safety",
|
||||
"climate": "Climate"
|
||||
},
|
||||
"state": {
|
||||
@@ -695,11 +695,6 @@
|
||||
"remove_device_id": "Remove device",
|
||||
"remove_entity_id": "Remove entity",
|
||||
"remove_label_id": "Remove label",
|
||||
"floor_not_found": "Floor not found",
|
||||
"area_not_found": "Area not found",
|
||||
"device_not_found": "Device not found",
|
||||
"entity_not_found": "Entity not found",
|
||||
"label_not_found": "Label not found",
|
||||
"devices_count": "{count} {count, plural,\n one {device}\n other {devices}\n}",
|
||||
"entities_count": "{count} {count, plural,\n one {entity}\n other {entities}\n}",
|
||||
"target_details": "Target details",
|
||||
@@ -1434,7 +1429,6 @@
|
||||
"back_to_info": "Back to info",
|
||||
"info": "Information",
|
||||
"related": "Related",
|
||||
"add_entity_to": "Add to",
|
||||
"history": "History",
|
||||
"aggregate": "5-minute aggregated",
|
||||
"logbook": "Activity",
|
||||
@@ -1451,10 +1445,6 @@
|
||||
"last_action": "Last action",
|
||||
"last_triggered": "Last triggered"
|
||||
},
|
||||
"add_to": {
|
||||
"no_actions": "No actions available",
|
||||
"action_failed": "Failed to perform the action {error}"
|
||||
},
|
||||
"sun": {
|
||||
"azimuth": "Azimuth",
|
||||
"elevation": "Elevation",
|
||||
@@ -3076,7 +3066,6 @@
|
||||
"remove_co2_signal": "Remove Electricity Maps integration",
|
||||
"add_co2_signal": "Add Electricity Maps integration",
|
||||
"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.",
|
||||
"from": {
|
||||
"header": "Configure grid consumption",
|
||||
"paragraph": "Grid consumption is the energy that flows from the energy grid to your home.",
|
||||
@@ -6991,7 +6980,7 @@
|
||||
"lights": "Lights",
|
||||
"other_lights": "Other lights"
|
||||
},
|
||||
"security": {
|
||||
"safety": {
|
||||
"devices": "Devices",
|
||||
"other_devices": "Other devices"
|
||||
},
|
||||
@@ -7560,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",
|
||||
@@ -7789,9 +7784,7 @@
|
||||
},
|
||||
"iframe": {
|
||||
"name": "Webpage",
|
||||
"description": "The Webpage card allows you to embed your favorite webpage right into Home Assistant.",
|
||||
"hide_background": "Hide background",
|
||||
"hide_background_helper": "Useful for pages which allow a transparent background."
|
||||
"description": "The Webpage card allows you to embed your favorite webpage right into Home Assistant."
|
||||
},
|
||||
"light": {
|
||||
"name": "Light",
|
||||
|
||||
@@ -1,26 +1,10 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { render } from "lit";
|
||||
|
||||
const removeElement = (launchScreenElement: HTMLElement) => {
|
||||
launchScreenElement.classList.add("removing");
|
||||
setTimeout(() => {
|
||||
launchScreenElement.parentElement?.removeChild(launchScreenElement);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
export const removeLaunchScreen = () => {
|
||||
const launchScreenElement = document.getElementById("ha-launch-screen");
|
||||
if (!launchScreenElement?.parentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.startViewTransition) {
|
||||
document.startViewTransition(() => {
|
||||
removeElement(launchScreenElement);
|
||||
});
|
||||
} else {
|
||||
// Fallback: Direct removal without transition
|
||||
removeElement(launchScreenElement);
|
||||
if (launchScreenElement) {
|
||||
launchScreenElement.parentElement!.removeChild(launchScreenElement);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
209
test/common/datetime/check_time.test.ts
Normal file
209
test/common/datetime/check_time.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user