Compare commits

..

18 Commits

Author SHA1 Message Date
Aidan Timson
b17dc53d27 Format 2025-11-04 14:08:33 +00:00
Aidan Timson
ae9f194868 SImplify 2025-11-04 14:04:09 +00:00
Aidan Timson
c762f12ffd Refactor 2025-11-04 12:54:19 +00:00
Aidan Timson
cf9f4afcdc Remove duplicate checks 2025-11-04 12:54:19 +00:00
Aidan Timson
9f394a4ed6 Remove multiple shortcuts per disposer 2025-11-04 12:54:19 +00:00
Aidan Timson
9da1835a57 Fix misleading comment 2025-11-04 12:54:19 +00:00
Aidan Timson
8d3c36236f Explainers 2025-11-04 12:54:19 +00:00
Aidan Timson
b4d6ee105f Cleanup 2025-11-04 12:54:19 +00:00
Aidan Timson
ffcb78bc7b Cleanup 2025-11-04 12:54:19 +00:00
Aidan Timson
6f7be230d8 Make class 2025-11-04 12:54:19 +00:00
Aidan Timson
f5fa606ac2 foreach 2025-11-04 12:54:19 +00:00
Aidan Timson
b18c14117d foreach 2025-11-04 12:54:19 +00:00
Aidan Timson
e64366ea61 enableShortcuts check already in mixin, remove hass param 2025-11-04 12:54:19 +00:00
Aidan Timson
4c3e1430bf Drop examples 2025-11-04 12:54:19 +00:00
Aidan Timson
2019bb8ebc Manage disposers from manager and document methods 2025-11-04 12:54:19 +00:00
Aidan Timson
523c26a11a Switch to set and allow removal of all or a list 2025-11-04 12:54:18 +00:00
Aidan Timson
3a7d106bcc Migrate quick bar mixin to use helper 2025-11-04 12:54:18 +00:00
Aidan Timson
b6a841d109 Create keyboard shortcuts helper 2025-11-04 12:54:18 +00:00
29 changed files with 520 additions and 780 deletions

View File

@@ -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);

View File

@@ -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>
`;
}

View File

@@ -0,0 +1,66 @@
import { tinykeys } from "tinykeys";
import { canOverrideAlphanumericInput } from "../dom/can-override-input";
/**
* A function to handle a keyboard shortcut.
*/
export type ShortcutHandler = (event: KeyboardEvent) => void;
/**
* Configuration for a keyboard shortcut.
*/
export interface ShortcutConfig {
handler: ShortcutHandler;
/**
* If true, allows shortcuts even when text is selected.
* Default is false to avoid interrupting copy/paste.
*/
allowWhenTextSelected?: boolean;
}
/**
* Register keyboard shortcuts using tinykeys.
* Automatically blocks shortcuts in input fields and during text selection.
*/
function registerShortcuts(
shortcuts: Record<string, ShortcutConfig>
): () => void {
const wrappedShortcuts: Record<string, ShortcutHandler> = {};
Object.entries(shortcuts).forEach(([key, config]) => {
wrappedShortcuts[key] = (event: KeyboardEvent) => {
if (!canOverrideAlphanumericInput(event.composedPath())) {
return;
}
if (!config.allowWhenTextSelected && window.getSelection()?.toString()) {
return;
}
config.handler(event);
};
});
return tinykeys(window, wrappedShortcuts);
}
/**
* Manages keyboard shortcuts registration and cleanup.
*/
export class ShortcutManager {
private _disposer?: () => void;
/**
* Register keyboard shortcuts.
* Uses tinykeys syntax: https://github.com/jamiebuilds/tinykeys#usage
*/
public add(shortcuts: Record<string, ShortcutConfig>) {
this._disposer = registerShortcuts(shortcuts);
}
/**
* Remove all registered shortcuts.
*/
public remove() {
this._disposer?.();
this._disposer = undefined;
}
}

View File

@@ -89,14 +89,14 @@ export class HaChartBase extends LitElement {
private _lastTapTime?: number;
private _shouldResizeChart: boolean | number = false; // number means animation duration
private _shouldResizeChart = false;
// @ts-ignore
private _resizeController = new ResizeController(this, {
callback: () => {
if (this.chart) {
if (!this.chart.getZr().animation.isFinished()) {
this._shouldResizeChart = this._shouldResizeChart || true;
this._shouldResizeChart = true;
} else {
this.chart.resize();
}
@@ -213,7 +213,7 @@ export class HaChartBase extends LitElement {
)
) {
// custom legend changes may require a resize to layout properly
this._shouldResizeChart = RESIZE_ANIMATION_DURATION;
this._shouldResizeChart = true;
}
} else if (this._isTouchDevice && changedProps.has("_isZoomed")) {
chartOptions.dataZoom = this._getDataZoomConfig();
@@ -977,10 +977,9 @@ export class HaChartBase extends LitElement {
private _handleChartRenderFinished = () => {
if (this._shouldResizeChart) {
this.chart?.resize({
animation:
this._reducedMotion || typeof this._shouldResizeChart !== "number"
? undefined
: { duration: this._shouldResizeChart },
animation: this._reducedMotion
? undefined
: { duration: RESIZE_ANIMATION_DURATION },
});
this._shouldResizeChart = false;
}

View File

@@ -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%;

View File

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

View File

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

View File

@@ -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")

View File

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

View File

@@ -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"),
};

View File

@@ -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",

View File

@@ -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)}

View File

@@ -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) => {

View File

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

View File

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

View File

@@ -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",

View File

@@ -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";
});

View File

@@ -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%;

View File

@@ -323,7 +323,6 @@ export interface IframeCardConfig extends LovelaceCardConfig {
title?: string;
allow?: string;
url: string;
hide_background?: boolean;
}
export interface LightCardConfig extends LovelaceCardConfig {

View File

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

View File

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

View File

@@ -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: {

View File

@@ -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}`);

View File

@@ -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),
],
});
}

View File

@@ -162,7 +162,7 @@ export class HomeMainViewStrategy extends ReactiveElement {
generateEntityFilter(hass, filter)
);
const securityFilters = HOME_SUMMARIES_FILTERS.security.map((filter) =>
const safetyFilters = HOME_SUMMARIES_FILTERS.safety.map((filter) =>
generateEntityFilter(hass, filter)
);
@@ -170,7 +170,7 @@ export class HomeMainViewStrategy extends ReactiveElement {
const hasMediaPlayers =
findEntities(allEntities, mediaPlayerFilter).length > 0;
const hasClimate = findEntities(allEntities, climateFilters).length > 0;
const hasSecurity = findEntities(allEntities, securityFilters).length > 0;
const hasSafety = findEntities(allEntities, safetyFilters).length > 0;
const summaryCards: LovelaceCardConfig[] = [
hasLights &&
@@ -201,14 +201,14 @@ export class HomeMainViewStrategy extends ReactiveElement {
columns: 4,
},
} satisfies HomeSummaryCard),
hasSecurity &&
hasSafety &&
({
type: "home-summary",
summary: "security",
summary: "safety",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/security?historyBack=1",
navigation_path: "/safety?historyBack=1",
},
grid_options: {
rows: 2,

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -1,5 +1,4 @@
import type { PropertyValues } from "lit";
import { tinykeys } from "tinykeys";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { mainWindow } from "../common/dom/get_main_window";
@@ -12,9 +11,9 @@ import type { Constructor, HomeAssistant } from "../types";
import { storeState } from "../util/ha-pref-storage";
import { showToast } from "../util/toast";
import type { HassElement } from "./hass-element";
import { ShortcutManager } from "../common/keyboard/shortcuts";
import { extractSearchParamsObject } from "../common/url/search-params";
import { showVoiceCommandDialog } from "../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
import { canOverrideAlphanumericInput } from "../common/dom/can-override-input";
import { showShortcutsDialog } from "../dialogs/shortcuts/show-shortcuts-dialog";
import type { Redirects } from "../panels/my/ha-panel-my";
@@ -62,21 +61,22 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
}
private _registerShortcut() {
tinykeys(window, {
const shortcutManager = new ShortcutManager();
shortcutManager.add({
// Those are for latin keyboards that have e, c, m keys
e: (ev) => this._showQuickBar(ev),
c: (ev) => this._showQuickBar(ev, QuickBarMode.Command),
m: (ev) => this._createMyLink(ev),
a: (ev) => this._showVoiceCommandDialog(ev),
d: (ev) => this._showQuickBar(ev, QuickBarMode.Device),
e: { handler: (ev) => this._showQuickBar(ev) },
c: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Command) },
m: { handler: (ev) => this._createMyLink(ev) },
a: { handler: (ev) => this._showVoiceCommandDialog(ev) },
d: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Device) },
// Workaround see https://github.com/jamiebuilds/tinykeys/issues/130
"Shift+?": (ev) => this._showShortcutDialog(ev),
"Shift+?": { handler: (ev) => this._showShortcutDialog(ev) },
// Those are fallbacks for non-latin keyboards that don't have e, c, m keys (qwerty-based shortcuts)
KeyE: (ev) => this._showQuickBar(ev),
KeyC: (ev) => this._showQuickBar(ev, QuickBarMode.Command),
KeyM: (ev) => this._createMyLink(ev),
KeyA: (ev) => this._showVoiceCommandDialog(ev),
KeyD: (ev) => this._showQuickBar(ev, QuickBarMode.Device),
KeyE: { handler: (ev) => this._showQuickBar(ev) },
KeyC: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Command) },
KeyM: { handler: (ev) => this._createMyLink(ev) },
KeyA: { handler: (ev) => this._showVoiceCommandDialog(ev) },
KeyD: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Device) },
});
}
@@ -87,7 +87,6 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
private _showVoiceCommandDialog(e: KeyboardEvent) {
if (
!this.hass?.enableShortcuts ||
!canOverrideAlphanumericInput(e.composedPath()) ||
!this._conversation(this.hass.config.components)
) {
return;
@@ -105,7 +104,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
e: KeyboardEvent,
mode: QuickBarMode = QuickBarMode.Entity
) {
if (!this._canShowQuickBar(e)) {
if (!this._canShowQuickBar()) {
return;
}
@@ -118,7 +117,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
}
private _showShortcutDialog(e: KeyboardEvent) {
if (!this._canShowQuickBar(e)) {
if (!this._canShowQuickBar()) {
return;
}
@@ -131,10 +130,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
}
private async _createMyLink(e: KeyboardEvent) {
if (
!this.hass?.enableShortcuts ||
!canOverrideAlphanumericInput(e.composedPath())
) {
if (!this.hass?.enableShortcuts) {
return;
}
@@ -193,11 +189,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
});
}
private _canShowQuickBar(e: KeyboardEvent) {
return (
this.hass?.user?.is_admin &&
this.hass.enableShortcuts &&
canOverrideAlphanumericInput(e.composedPath())
);
private _canShowQuickBar() {
return this.hass?.user?.is_admin && this.hass.enableShortcuts;
}
};

View File

@@ -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",
@@ -6985,7 +6980,7 @@
"lights": "Lights",
"other_lights": "Other lights"
},
"security": {
"safety": {
"devices": "Devices",
"other_devices": "Other devices"
},
@@ -7783,9 +7778,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",