Compare commits

..

26 Commits

Author SHA1 Message Date
Wendelin 06355f6093 Fix target details for labels 2025-09-30 16:09:58 +02:00
Wendelin 36bcaade22 Fix target details button and expansion panel keyboard focus 2025-09-30 14:06:57 +02:00
Wendelin cdf5d2c899 Merge branch 'dev' of github.com:home-assistant/frontend into target-selected-value 2025-09-30 13:48:06 +02:00
renovate[bot] d746dc5752 Pin Node.js to 22.20.0 (#27258)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 11:53:51 +02:00
Wendelin 5f53e1e71c Update WA to fix tab group scrolling (#27255) 2025-09-30 11:36:17 +02:00
Wendelin 3da82df093 Update node nvm to latest LTS (#27256) 2025-09-30 11:35:48 +02:00
Wendelin 6cdf9ba1ab Merge branch 'dev' of github.com:home-assistant/frontend into target-selected-value 2025-09-30 11:22:37 +02:00
Jan-Philipp Benecke 4cedfffb71 Let text scroll in markdown card (#27250)
* Let text overflow in markdown card

* Update src/panels/lovelace/cards/hui-markdown-card.ts

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2025-09-30 09:37:15 +02:00
dcapslock 1e1514e7da forwardHaptic on node rather than window. (#27251)
forwardHaptic on node rather than window. Allows for capturing for custom cards.
2025-09-30 07:49:32 +03:00
Jan-Philipp Benecke 60e07075bc Refactor ha-config-labels to use styleMap (#27248) 2025-09-29 21:05:17 +02:00
Jan-Philipp Benecke c998086474 Fix --ha-space-13 spacing token (#27246) 2025-09-29 20:16:29 +02:00
Wendelin 32e4c23c1a Merge branch 'dev' of github.com:home-assistant/frontend into target-selected-value 2025-09-25 09:17:09 +02:00
Wendelin cf97566669 Fix dialog title 2025-09-18 16:59:37 +02:00
Wendelin c68d73b1ef Add overview dialog 2025-09-18 11:08:52 +02:00
Wendelin 89ef753309 Fix filtering 2025-09-17 15:18:49 +02:00
Wendelin b149ddc3d4 Enhance target picker with filtering options for devices and entities 2025-09-16 17:31:46 +02:00
Wendelin a2e99de828 Do not show entities count for sub entries 2025-09-16 15:51:18 +02:00
Wendelin e3655feda0 Keep chips in history and logbook 2025-09-16 13:14:11 +02:00
Wendelin a5be92c277 Merge branch 'dev' of github.com:home-assistant/frontend into target-selected-value 2025-09-16 11:08:44 +02:00
Wendelin 043849b057 Group floor and area, add label icon, remove remove group 2025-09-12 14:33:41 +02:00
Wendelin 9a69566000 Fix sublist 2025-09-12 12:02:01 +02:00
Wendelin 9cdb57476a Merge branch 'dev' of github.com:home-assistant/frontend into target-selected-value 2025-09-11 15:59:07 +02:00
Wendelin f8d90d003e fix entity domain name 2025-09-11 14:36:44 +02:00
Wendelin f53ee52b0e Fix typo 2025-09-11 12:27:05 +02:00
Wendelin f8cc1531e5 Use extractFromTarget 2025-09-11 12:25:02 +02:00
Wendelin 11f65ef0f7 Add new target selected value view 2025-09-09 15:38:48 +02:00
61 changed files with 2053 additions and 1651 deletions
+1 -1
View File
@@ -1 +1 @@
lts/iron
22.20.0
+1 -1
View File
@@ -52,7 +52,7 @@
"@fullcalendar/list": "6.1.19",
"@fullcalendar/luxon3": "6.1.19",
"@fullcalendar/timegrid": "6.1.19",
"@home-assistant/webawesome": "3.0.0-beta.4.ha.3",
"@home-assistant/webawesome": "3.0.0-beta.6.ha.0",
"@lezer/highlight": "1.2.1",
"@lit-labs/motion": "1.0.9",
"@lit-labs/observers": "2.0.6",
-19
View File
@@ -122,22 +122,3 @@ export const generateEntityFilter = (
return true;
};
};
export const findEntities = (
entities: string[],
filters: EntityFilterFunc[]
): string[] => {
const seen = new Set<string>();
const results: string[] = [];
for (const filter of filters) {
for (const entity of entities) {
if (filter(entity) && !seen.has(entity)) {
seen.add(entity);
results.push(entity);
}
}
}
return results;
};
+1 -1
View File
@@ -112,7 +112,7 @@ export class HaEntityToggle extends LitElement {
if (!this.hass || !this.stateObj) {
return;
}
forwardHaptic("light");
forwardHaptic(this, "light");
const stateDomain = computeStateDomain(this.stateObj);
let serviceDomain;
let service;
+6
View File
@@ -49,6 +49,7 @@ export class HaExpansionPanel extends LitElement {
tabindex=${this.noCollapse ? -1 : 0}
aria-expanded=${this.expanded}
aria-controls="sect1"
part="summary"
>
${this.leftChevron ? chevronIcon : nothing}
<slot name="leading-icon"></slot>
@@ -170,6 +171,11 @@ export class HaExpansionPanel extends LitElement {
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
border-radius: var(--ha-border-radius-circle);
}
#summary:focus-visible ha-svg-icon.summary-icon {
background-color: var(--ha-color-fill-neutral-normal-active);
}
:host([left-chevron]) .summary-icon,
+1 -4
View File
@@ -32,10 +32,7 @@ export class HaIconOverflowMenu extends LitElement {
@property({ type: Boolean }) public narrow = false;
protected render(): TemplateResult | typeof nothing {
if (this.items.length === 0) {
return nothing;
}
protected render(): TemplateResult {
return html`
${this.narrow
? html` <!-- Collapsed representation for small screens -->
+1 -1
View File
@@ -15,7 +15,7 @@ export class HaSwitch extends SwitchBase {
super.firstUpdated();
this.addEventListener("change", () => {
if (this.haptic) {
forwardHaptic("light");
forwardHaptic(this, "light");
}
});
}
+289 -350
View File
@@ -1,39 +1,21 @@
// @ts-ignore
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
import "@material/mwc-menu/mwc-menu-surface";
import {
mdiClose,
mdiDevices,
mdiHome,
mdiLabel,
mdiPlus,
mdiTextureBox,
mdiUnfoldMoreVertical,
} from "@mdi/js";
import { mdiPlus } from "@mdi/js";
import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
import type {
HassEntity,
HassServiceTarget,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import type { HassServiceTarget } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing, unsafeCSS } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ensureArray } from "../common/array/ensure-array";
import { computeCssColor } from "../common/color/compute-color";
import { hex2rgb } from "../common/color/convert-color";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { computeDeviceNameDisplay } from "../common/entity/compute_device_name";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { isValidEntityId } from "../common/entity/valid_entity_id";
import type { AreaRegistryEntry } from "../data/area_registry";
import type { DeviceRegistryEntry } from "../data/device_registry";
import type { EntityRegistryDisplayEntry } from "../data/entity_registry";
import type { LabelRegistryEntry } from "../data/label_registry";
import { subscribeLabelRegistry } from "../data/label_registry";
import {
areaMeetsFilter,
deviceMeetsFilter,
entityRegMeetsFilter,
} from "../data/target";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import type { HomeAssistant } from "../types";
import "./device/ha-device-picker";
@@ -41,12 +23,14 @@ import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./entity/ha-entity-picker";
import type { HaEntityPickerEntityFilterFunc } from "./entity/ha-entity-picker";
import "./ha-area-floor-picker";
import { floorDefaultIconPath } from "./ha-floor-icon";
import "./ha-icon-button";
import "./ha-input-helper-text";
import "./ha-label-picker";
import "./ha-svg-icon";
import "./ha-tooltip";
import "./target-picker/ha-target-picker-chips-selection";
import "./target-picker/ha-target-picker-item-group";
import type { TargetType } from "./target-picker/ha-target-picker-item-row";
@customElement("ha-target-picker")
export class HaTargetPicker extends SubscribeMixin(LitElement) {
@@ -58,6 +42,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public compact = false;
@property({ attribute: false, type: Array }) public createDomains?: string[];
/**
@@ -96,18 +82,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@query(".add-container", true) private _addContainer?: HTMLDivElement;
@state() private _labels?: LabelRegistryEntry[];
private _opened = false;
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeLabelRegistry(this.hass.connection, (labels) => {
this._labels = labels;
}),
];
}
protected render() {
if (this.addOnTop) {
return html` ${this._renderChips()} ${this._renderItems()} `;
@@ -116,89 +92,154 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
}
private _renderItems() {
if (
!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="mdc-chip-set items">
${this.value?.floor_id
? ensureArray(this.value.floor_id).map((floor_id) => {
const floor = this.hass.floors[floor_id];
return this._renderChip(
"floor_id",
floor_id,
floor?.name || floor_id,
undefined,
floor?.icon,
floor ? floorDefaultIconPath(floor) : mdiHome
);
})
: ""}
${this.value?.area_id
? ensureArray(this.value.area_id).map((area_id) => {
const area = this.hass.areas![area_id];
return this._renderChip(
"area_id",
area_id,
area?.name || area_id,
undefined,
area?.icon,
mdiTextureBox
);
})
: nothing}
${this.value?.device_id
? ensureArray(this.value.device_id).map((device_id) => {
const device = this.hass.devices![device_id];
return this._renderChip(
"device_id",
device_id,
device
? computeDeviceNameDisplay(device, this.hass)
: device_id,
undefined,
undefined,
mdiDevices
);
})
: nothing}
${this.value?.entity_id
? ensureArray(this.value.entity_id).map((entity_id) => {
const entity = this.hass.states[entity_id];
return this._renderChip(
"entity_id",
entity_id,
entity ? computeStateName(entity) : entity_id,
entity
);
})
: nothing}
${this.value?.label_id
? ensureArray(this.value.label_id).map((label_id) => {
const label = this._labels?.find(
(lbl) => lbl.label_id === label_id
);
let color = label?.color
? computeCssColor(label.color)
: undefined;
if (color?.startsWith("var(")) {
const computedStyles = getComputedStyle(this);
color = computedStyles.getPropertyValue(
color.substring(4, color.length - 1)
);
}
if (color?.startsWith("#")) {
color = hex2rgb(color).join(",");
}
return this._renderChip(
"label_id",
label_id,
label ? label.name : label_id,
undefined,
label?.icon,
mdiLabel,
color
);
})
: nothing}
</div>
${this.compact
? html`<div class="mdc-chip-set items">
${this.value?.floor_id
? ensureArray(this.value.floor_id).map(
(floor_id) => html`
<ha-target-picker-chips-selection
.hass=${this.hass}
.type=${"floor"}
.itemId=${floor_id}
@remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand}
></ha-target-picker-chips-selection>
`
)
: nothing}
${this.value?.area_id
? ensureArray(this.value.area_id).map(
(area_id) => html`
<ha-target-picker-chips-selection
.hass=${this.hass}
.type=${"area"}
.itemId=${area_id}
@remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand}
></ha-target-picker-chips-selection>
`
)
: nothing}
${this.value?.device_id
? ensureArray(this.value.device_id).map(
(device_id) => html`
<ha-target-picker-chips-selection
.hass=${this.hass}
.type=${"device"}
.itemId=${device_id}
@remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand}
></ha-target-picker-chips-selection>
`
)
: nothing}
${this.value?.entity_id
? ensureArray(this.value.entity_id).map(
(entity_id) => html`
<ha-target-picker-chips-selection
.hass=${this.hass}
.type=${"entity"}
.itemId=${entity_id}
@remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand}
></ha-target-picker-chips-selection>
`
)
: nothing}
${this.value?.label_id
? ensureArray(this.value.label_id).map(
(label_id) => html`
<ha-target-picker-chips-selection
.hass=${this.hass}
.type=${"label"}
.itemId=${label_id}
@remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand}
></ha-target-picker-chips-selection>
`
)
: nothing}
</div>`
: html`<div class="item-groups">
${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),
}}
.collapsed=${this.compact}
.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) }}
.collapsed=${this.compact}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
>
</ha-target-picker-item-group>
`
: nothing}
${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) }}
.collapsed=${this.compact}
.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) }}
.collapsed=${this.compact}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
>
</ha-target-picker-item-group>
`
: nothing}
</div>`}
`;
}
@@ -299,85 +340,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
this._addMode = ev.currentTarget.type;
}
private _renderChip(
type: "floor_id" | "area_id" | "device_id" | "entity_id" | "label_id",
id: string,
name: string,
entityState?: HassEntity,
icon?: string | null,
fallbackIconPath?: string,
color?: string
) {
return html`
<div
class="mdc-chip ${classMap({
[type]: true,
})}"
style=${color
? `--color: rgb(${color}); --background-color: rgba(${color}, .5)`
: ""}
>
${icon
? html`<ha-icon
class="mdc-chip__icon mdc-chip__icon--leading"
.icon=${icon}
></ha-icon>`
: fallbackIconPath
? html`<ha-svg-icon
class="mdc-chip__icon mdc-chip__icon--leading"
.path=${fallbackIconPath}
></ha-svg-icon>`
: ""}
${entityState
? html`<ha-state-icon
class="mdc-chip__icon mdc-chip__icon--leading"
.hass=${this.hass}
.stateObj=${entityState}
></ha-state-icon>`
: ""}
<span role="gridcell">
<span role="button" tabindex="0" class="mdc-chip__primary-action">
<span class="mdc-chip__text">${name}</span>
</span>
</span>
${type === "entity_id"
? ""
: html`<span role="gridcell">
<ha-tooltip .for="expand-${id}"
>${this.hass.localize(
`ui.components.target-picker.expand_${type}`
)}
</ha-tooltip>
<ha-icon-button
class="expand-btn mdc-chip__icon mdc-chip__icon--trailing"
.label=${this.hass.localize(
"ui.components.target-picker.expand"
)}
.path=${mdiUnfoldMoreVertical}
hide-title
.id="expand-${id}"
.type=${type}
@click=${this._handleExpand}
></ha-icon-button>
</span>`}
<span role="gridcell">
<ha-tooltip .for="remove-${id}">
${this.hass.localize(`ui.components.target-picker.remove_${type}`)}
</ha-tooltip>
<ha-icon-button
class="mdc-chip__icon mdc-chip__icon--trailing"
.label=${this.hass.localize("ui.components.target-picker.remove")}
.path=${mdiClose}
hide-title
.id="remove-${id}"
.type=${type}
@click=${this._handleRemove}
></ha-icon-button>
</span>
</div>
`;
}
private _renderPicker() {
if (!this._addMode) {
return nothing;
@@ -520,76 +482,138 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
});
}
private _handleRemove(ev) {
const { type, id } = ev.detail;
fireEvent(this, "value-changed", {
value: this._removeItem(this.value, type, id),
});
}
private _handleExpand(ev) {
const target = ev.currentTarget as any;
const id = target.id.replace(/^expand-/, "");
const type = ev.detail.type;
const itemId = ev.detail.id;
const newAreas: string[] = [];
const newDevices: string[] = [];
const newEntities: string[] = [];
if (target.type === "floor_id") {
if (type === "floor") {
Object.values(this.hass.areas).forEach((area) => {
if (
area.floor_id === id &&
area.floor_id === itemId &&
!this.value!.area_id?.includes(area.area_id) &&
this._areaMeetsFilter(area)
areaMeetsFilter(
area,
this.hass.devices,
this.hass.entities,
this.deviceFilter,
this.includeDomains,
this.includeDeviceClasses,
this.hass.states,
this.entityFilter
)
) {
newAreas.push(area.area_id);
}
});
} else if (target.type === "area_id") {
} else if (type === "area") {
Object.values(this.hass.devices).forEach((device) => {
if (
device.area_id === id &&
device.area_id === itemId &&
!this.value!.device_id?.includes(device.id) &&
this._deviceMeetsFilter(device)
deviceMeetsFilter(
device,
this.hass.entities,
this.deviceFilter,
this.includeDomains,
this.includeDeviceClasses,
this.hass.states,
this.entityFilter
)
) {
newDevices.push(device.id);
}
});
Object.values(this.hass.entities).forEach((entity) => {
if (
entity.area_id === id &&
entity.area_id === itemId &&
!this.value!.entity_id?.includes(entity.entity_id) &&
this._entityRegMeetsFilter(entity)
entityRegMeetsFilter(
entity,
false,
this.includeDomains,
this.includeDeviceClasses,
this.hass.states,
this.entityFilter
)
) {
newEntities.push(entity.entity_id);
}
});
} else if (target.type === "device_id") {
} else if (type === "device") {
Object.values(this.hass.entities).forEach((entity) => {
if (
entity.device_id === id &&
entity.device_id === itemId &&
!this.value!.entity_id?.includes(entity.entity_id) &&
this._entityRegMeetsFilter(entity)
entityRegMeetsFilter(
entity,
false,
this.includeDomains,
this.includeDeviceClasses,
this.hass.states,
this.entityFilter
)
) {
newEntities.push(entity.entity_id);
}
});
} else if (target.type === "label_id") {
} else if (type === "label") {
Object.values(this.hass.areas).forEach((area) => {
if (
area.labels.includes(id) &&
area.labels.includes(itemId) &&
!this.value!.area_id?.includes(area.area_id) &&
this._areaMeetsFilter(area)
areaMeetsFilter(
area,
this.hass.devices,
this.hass.entities,
this.deviceFilter,
this.includeDomains,
this.includeDeviceClasses,
this.hass.states,
this.entityFilter
)
) {
newAreas.push(area.area_id);
}
});
Object.values(this.hass.devices).forEach((device) => {
if (
device.labels.includes(id) &&
device.labels.includes(itemId) &&
!this.value!.device_id?.includes(device.id) &&
this._deviceMeetsFilter(device)
deviceMeetsFilter(
device,
this.hass.entities,
this.deviceFilter,
this.includeDomains,
this.includeDeviceClasses,
this.hass.states,
this.entityFilter
)
) {
newDevices.push(device.id);
}
});
Object.values(this.hass.entities).forEach((entity) => {
if (
entity.labels.includes(id) &&
entity.labels.includes(itemId) &&
!this.value!.entity_id?.includes(entity.entity_id) &&
this._entityRegMeetsFilter(entity, true)
entityRegMeetsFilter(
entity,
true,
this.includeDomains,
this.includeDeviceClasses,
this.hass.states,
this.entityFilter
)
) {
newEntities.push(entity.entity_id);
}
@@ -607,18 +631,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
if (newAreas.length) {
value = this._addItems(value, "area_id", newAreas);
}
value = this._removeItem(value, target.type, id);
value = this._removeItem(value, type, itemId);
fireEvent(this, "value-changed", { value });
}
private _handleRemove(ev) {
const target = ev.currentTarget as any;
const id = target.id.replace(/^remove-/, "");
fireEvent(this, "value-changed", {
value: this._removeItem(this.value, target.type, id),
});
}
private _addItems(
value: this["value"],
type: string,
@@ -632,20 +648,22 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
private _removeItem(
value: this["value"],
type: string,
type: TargetType,
id: string
): this["value"] {
const newVal = ensureArray(value![type])!.filter(
const typeId = `${type}_id`;
const newVal = ensureArray(value![typeId])!.filter(
(val) => String(val) !== id
);
if (newVal.length) {
return {
...value,
[type]: newVal,
[typeId]: newVal,
};
}
const val = { ...value }!;
delete val[type];
delete val[typeId];
if (Object.keys(val).length) {
return val;
}
@@ -677,83 +695,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
ev.preventDefault();
}
private _areaMeetsFilter(area: AreaRegistryEntry): boolean {
const areaDevices = Object.values(this.hass.devices).filter(
(device) => device.area_id === area.area_id
);
if (areaDevices.some((device) => this._deviceMeetsFilter(device))) {
return true;
}
const areaEntities = Object.values(this.hass.entities).filter(
(entity) => entity.area_id === area.area_id
);
if (areaEntities.some((entity) => this._entityRegMeetsFilter(entity))) {
return true;
}
return false;
}
private _deviceMeetsFilter(device: DeviceRegistryEntry): boolean {
const devEntities = Object.values(this.hass.entities).filter(
(entity) => entity.device_id === device.id
);
if (!devEntities.some((entity) => this._entityRegMeetsFilter(entity))) {
return false;
}
if (this.deviceFilter) {
if (!this.deviceFilter(device)) {
return false;
}
}
return true;
}
private _entityRegMeetsFilter(
entity: EntityRegistryDisplayEntry,
includeSecondary = false
): boolean {
if (entity.hidden || (entity.entity_category && !includeSecondary)) {
return false;
}
if (
this.includeDomains &&
!this.includeDomains.includes(computeDomain(entity.entity_id))
) {
return false;
}
if (this.includeDeviceClasses) {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
if (
!stateObj.attributes.device_class ||
!this.includeDeviceClasses!.includes(stateObj.attributes.device_class)
) {
return false;
}
}
if (this.entityFilter) {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
if (!this.entityFilter!(stateObj)) {
return false;
}
}
return true;
}
static get styles(): CSSResultGroup {
return css`
${unsafeCSS(chipStyles)}
@@ -765,6 +706,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
}
.mdc-chip-set {
padding: 4px 0;
gap: 8px;
}
.mdc-chip-set .mdc-chip {
margin: 0;
}
.mdc-chip.add {
color: rgba(0, 0, 0, 0.87);
@@ -786,7 +731,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
border-radius: 50%;
background: var(--secondary-text-color);
}
.mdc-chip__icon.mdc-chip__icon--trailing {
.mdc-chip__icon.mdc-chip__icon--2 {
width: 16px;
height: 16px;
--mdc-icon-size: 14px;
@@ -807,46 +752,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
margin-inline-end: 4px !important;
direction: var(--direction);
}
.expand-btn {
margin-right: 0;
margin-inline-end: 0;
margin-inline-start: initial;
}
.mdc-chip.area_id:not(.add),
.mdc-chip.floor_id:not(.add) {
border: 1px solid #fed6a4;
background: var(--card-background-color);
}
.mdc-chip.area_id:not(.add) .mdc-chip__icon--leading,
.mdc-chip.area_id.add,
.mdc-chip.floor_id:not(.add) .mdc-chip__icon--leading,
.mdc-chip.floor_id.add {
background: #fed6a4;
}
.mdc-chip.device_id:not(.add) {
border: 1px solid #a8e1fb;
background: var(--card-background-color);
}
.mdc-chip.device_id:not(.add) .mdc-chip__icon--leading,
.mdc-chip.device_id.add {
background: #a8e1fb;
}
.mdc-chip.entity_id:not(.add) {
border: 1px solid #d2e7b9;
background: var(--card-background-color);
}
.mdc-chip.entity_id:not(.add) .mdc-chip__icon--leading,
.mdc-chip.entity_id.add {
background: #d2e7b9;
}
.mdc-chip.label_id:not(.add) {
border: 1px solid var(--color, #e0e0e0);
background: var(--card-background-color);
}
.mdc-chip.label_id:not(.add) .mdc-chip__icon--leading,
.mdc-chip.label_id.add {
background: var(--background-color, #e0e0e0);
}
.mdc-chip:hover {
z-index: 5;
}
@@ -863,8 +768,30 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
display: block;
width: 100%;
}
ha-tooltip {
--ha-tooltip-arrow-size: 0;
.item-groups {
overflow: hidden;
border: 2px solid var(--divider-color);
border-radius: var(--ha-border-radius-lg);
}
:host([compact]) .mdc-chip.area_id:not(.add),
.mdc-chip.floor_id:not(.add) {
border: 1px solid #fed6a4;
background: var(--card-background-color);
}
:host([compact]) .mdc-chip.area_id.add,
:host([compact]) .mdc-chip.floor_id.add {
background: #fed6a4;
}
:host([compact]) .mdc-chip.device_id.add {
background: #a8e1fb;
}
:host([compact]) .mdc-chip.entity_id.add {
background: #d2e7b9;
}
:host([compact]) .mdc-chip.label_id.add {
background: var(--background-color, #e0e0e0);
}
`;
}
@@ -874,4 +801,16 @@ declare global {
interface HTMLElementTagNameMap {
"ha-target-picker": HaTargetPicker;
}
interface HASSDomEvents {
"remove-target-item": {
type: string;
id: string;
};
"expand-target-item": {
type: string;
id: string;
};
"remove-target-group": string;
}
}
@@ -0,0 +1,125 @@
import { ContextProvider } from "@lit/context";
import { mdiClose } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { labelsContext } from "../../../data/context";
import { subscribeLabelRegistry } from "../../../data/label_registry";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../types";
import "../../ha-dialog-header";
import "../../ha-icon-button";
import "../../ha-icon-next";
import "../../ha-md-dialog";
import type { HaMdDialog } from "../../ha-md-dialog";
import "../../ha-md-list";
import "../../ha-md-list-item";
import "../../ha-svg-icon";
import "../ha-target-picker-item-row";
import type { TargetDetailsDialogParams } from "./show-dialog-target-details";
@customElement("ha-dialog-target-details")
class DialogTargetDetails
extends SubscribeMixin(LitElement)
implements HassDialog
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: TargetDetailsDialogParams;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
private _labelsContext = new ContextProvider(this, {
context: labelsContext,
initialValue: [],
});
public showDialog(params: TargetDetailsDialogParams): void {
this._params = params;
}
public closeDialog() {
this._dialog?.close();
return true;
}
private _dialogClosed() {
fireEvent(this, "dialog-closed", { dialog: this.localName });
this._params = undefined;
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeLabelRegistry(this.hass.connection!, (labels) => {
this._labelsContext.setValue(labels);
}),
];
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-md-dialog open @closed=${this._dialogClosed}>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
@click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title"
>${this.hass.localize(
"ui.components.target-picker.target_details"
)}</span
>
<span slot="subtitle"
>${this.hass.localize(
`ui.components.target-picker.type.${this._params.type}`
)}:
${this._params.title}</span
>
</ha-dialog-header>
<div slot="content">
<ha-target-picker-item-row
.hass=${this.hass}
.type=${this._params.type}
.itemId=${this._params.itemId}
.deviceFilter=${this._params.deviceFilter}
.entityFilter=${this._params.entityFilter}
.includeDomains=${this._params.includeDomains}
.includeDeviceClasses=${this._params.includeDeviceClasses}
expand
></ha-target-picker-item-row>
</div>
</ha-md-dialog>
`;
}
static styles = css`
ha-md-dialog {
min-width: 400px;
max-height: 90%;
--dialog-content-padding: 8px 24px
max(var(--safe-area-inset-bottom, 0px), 32px);
}
@media all and (max-width: 600px), all and (max-height: 500px) {
ha-md-dialog {
--md-dialog-container-shape: 0;
min-width: 100%;
min-height: 100%;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-dialog-target-details": DialogTargetDetails;
}
}
@@ -0,0 +1,28 @@
import { fireEvent } from "../../../common/dom/fire_event";
import type { HaDevicePickerDeviceFilterFunc } from "../../device/ha-device-picker";
import type { HaEntityPickerEntityFilterFunc } from "../../entity/ha-entity-picker";
import type { TargetType } from "../ha-target-picker-item-row";
export type NewBackupType = "automatic" | "manual";
export interface TargetDetailsDialogParams {
title: string;
type: TargetType;
itemId: string;
deviceFilter?: HaDevicePickerDeviceFilterFunc;
entityFilter?: HaEntityPickerEntityFilterFunc;
includeDomains?: string[];
includeDeviceClasses?: string[];
}
export const loadTargetDetailsDialog = () => import("./dialog-target-details");
export const showTargetDetailsDialog = (
element: HTMLElement,
params: TargetDetailsDialogParams
) =>
fireEvent(element, "show-dialog", {
dialogTag: "ha-dialog-target-details",
dialogImport: loadTargetDetailsDialog,
dialogParams: params,
});
@@ -0,0 +1,354 @@
import { consume } from "@lit/context";
// @ts-ignore
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
import {
mdiClose,
mdiDevices,
mdiHome,
mdiLabel,
mdiTextureBox,
mdiUnfoldMoreVertical,
} from "@mdi/js";
import { css, html, LitElement, nothing, unsafeCSS } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../common/color/compute-color";
import { hex2rgb } from "../../common/color/convert-color";
import { fireEvent } from "../../common/dom/fire_event";
import {
computeDeviceName,
computeDeviceNameDisplay,
} from "../../common/entity/compute_device_name";
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 { getConfigEntry } from "../../data/config_entries";
import { labelsContext } from "../../data/context";
import { domainToName } from "../../data/integration";
import type { LabelRegistryEntry } from "../../data/label_registry";
import type { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import { floorDefaultIconPath } from "../ha-floor-icon";
import "../ha-icon";
import "../ha-icon-button";
import "../ha-md-list";
import "../ha-md-list-item";
import "../ha-state-icon";
import "../ha-tooltip";
import type { TargetType } from "./ha-target-picker-item-row";
@customElement("ha-target-picker-chips-selection")
export class HaTargetPickerChipsSelection extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ reflect: true }) public type!: TargetType;
@property({ attribute: "item-id" }) public itemId!: string;
@state() private _domainName?: string;
@state() private _iconImg?: string;
@state()
@consume({ context: labelsContext, subscribe: true })
_labelRegistry!: LabelRegistryEntry[];
protected render() {
const { name, iconPath, fallbackIconPath, stateObject, color } =
this._itemData(this.type, this.itemId);
return html`
<div
class="mdc-chip ${classMap({
[this.type]: true,
})}"
style=${color
? `--color: rgb(${color}); --background-color: rgba(${color}, .5)`
: ""}
>
${iconPath
? html`<ha-icon
class="mdc-chip__icon mdc-chip__icon--leading"
.icon=${iconPath}
></ha-icon>`
: this._iconImg
? html`<img
class="mdc-chip__icon mdc-chip__icon--leading"
alt=${this._domainName || ""}
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${this._iconImg}
/>`
: fallbackIconPath
? html`<ha-svg-icon
class="mdc-chip__icon mdc-chip__icon--leading"
.path=${fallbackIconPath}
></ha-svg-icon>`
: stateObject
? html`<ha-state-icon
class="mdc-chip__icon mdc-chip__icon--leading"
.hass=${this.hass}
.stateObj=${stateObject}
></ha-state-icon>`
: nothing}
<span role="gridcell">
<span role="button" tabindex="0" class="mdc-chip__primary-action">
<span id="title-${this.itemId}" class="mdc-chip__text"
>${name}</span
>
</span>
</span>
${this.type === "entity"
? nothing
: html`<span role="gridcell">
<ha-tooltip .for="expand-${this.itemId}"
>${this.hass.localize(
`ui.components.target-picker.expand_${this.type}_id`
)}
</ha-tooltip>
<ha-icon-button
class="expand-btn mdc-chip__icon mdc-chip__icon--trailing"
.label=${this.hass.localize(
"ui.components.target-picker.expand"
)}
.path=${mdiUnfoldMoreVertical}
hide-title
.id="expand-${this.itemId}"
.type=${this.type}
@click=${this._handleExpand}
></ha-icon-button>
</span>`}
<span role="gridcell">
<ha-tooltip .for="remove-${this.itemId}">
${this.hass.localize(
`ui.components.target-picker.remove_${this.type}_id`
)}
</ha-tooltip>
<ha-icon-button
class="mdc-chip__icon mdc-chip__icon--trailing"
.label=${this.hass.localize("ui.components.target-picker.remove")}
.path=${mdiClose}
hide-title
.id="remove-${this.itemId}"
.type=${this.type}
@click=${this._removeItem}
></ha-icon-button>
</span>
</div>
`;
}
private _itemData = memoizeOne((type: TargetType, itemId: string) => {
if (type === "floor") {
const floor = this.hass.floors?.[itemId];
return {
name: floor?.name || itemId,
iconPath: floor?.icon,
fallbackIconPath: floor ? floorDefaultIconPath(floor) : mdiHome,
};
}
if (type === "area") {
const area = this.hass.areas?.[itemId];
return {
name: area?.name || itemId,
iconPath: area?.icon,
fallbackIconPath: mdiTextureBox,
};
}
if (type === "device") {
const device = this.hass.devices?.[itemId];
if (device.primary_config_entry) {
this._getDeviceDomain(device.primary_config_entry);
}
return {
name: device ? computeDeviceNameDisplay(device, this.hass) : itemId,
fallbackIconPath: mdiDevices,
};
}
if (type === "entity") {
this._setDomainName(computeDomain(itemId));
const stateObject = this.hass.states[itemId];
const entityName = computeEntityName(
stateObject,
this.hass.entities,
this.hass.devices
);
const { device } = getEntityContext(
stateObject,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const deviceName = device ? computeDeviceName(device) : undefined;
return {
name: entityName || deviceName || itemId,
stateObject,
};
}
// type label
const label = this._labelRegistry.find((lab) => lab.label_id === itemId);
let color = label?.color ? computeCssColor(label.color) : undefined;
if (color?.startsWith("var(")) {
const computedStyles = getComputedStyle(this);
color = computedStyles.getPropertyValue(
color.substring(4, color.length - 1)
);
}
if (color?.startsWith("#")) {
color = hex2rgb(color).join(",");
}
return {
name: label?.name || itemId,
iconPath: label?.icon,
fallbackIconPath: mdiLabel,
color,
};
});
private _setDomainName(domain: string) {
this._domainName = domainToName(this.hass.localize, domain);
}
private async _getDeviceDomain(configEntryId: string) {
try {
const data = await getConfigEntry(this.hass, configEntryId);
const domain = data.config_entry.domain;
this._iconImg = brandsUrl({
domain: domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
});
this._setDomainName(domain);
} catch {
// failed to load config entry -> ignore
}
}
private _removeItem(ev) {
ev.stopPropagation();
fireEvent(this, "remove-target-item", {
type: this.type,
id: this.itemId,
});
}
private _handleExpand(ev) {
ev.stopPropagation();
fireEvent(this, "expand-target-item", {
type: this.type,
id: this.itemId,
});
}
static styles = css`
${unsafeCSS(chipStyles)}
.mdc-chip {
color: var(--primary-text-color);
}
.mdc-chip.add {
color: rgba(0, 0, 0, 0.87);
}
.add-container {
position: relative;
display: inline-flex;
}
.mdc-chip:not(.add) {
cursor: default;
}
.mdc-chip ha-icon-button {
--mdc-icon-button-size: 24px;
display: flex;
align-items: center;
outline: none;
}
.mdc-chip ha-icon-button ha-svg-icon {
border-radius: 50%;
background: var(--secondary-text-color);
}
.mdc-chip__icon.mdc-chip__icon--trailing {
width: 16px;
height: 16px;
--mdc-icon-size: 14px;
color: var(--secondary-text-color);
margin-inline-start: 4px !important;
margin-inline-end: -4px !important;
direction: var(--direction);
}
.mdc-chip__icon--leading {
display: flex;
align-items: center;
justify-content: center;
--mdc-icon-size: 20px;
border-radius: 50%;
padding: 6px;
margin-left: -13px !important;
margin-inline-start: -13px !important;
margin-inline-end: 4px !important;
direction: var(--direction);
}
.expand-btn {
margin-right: 0;
margin-inline-end: 0;
margin-inline-start: initial;
}
.mdc-chip.area:not(.add),
.mdc-chip.floor:not(.add) {
border: 1px solid #fed6a4;
background: var(--card-background-color);
}
.mdc-chip.area:not(.add) .mdc-chip__icon--leading,
.mdc-chip.area.add,
.mdc-chip.floor:not(.add) .mdc-chip__icon--leading,
.mdc-chip.floor.add {
background: #fed6a4;
}
.mdc-chip.device:not(.add) {
border: 1px solid #a8e1fb;
background: var(--card-background-color);
}
.mdc-chip.device:not(.add) .mdc-chip__icon--leading,
.mdc-chip.device.add {
background: #a8e1fb;
}
.mdc-chip.entity:not(.add) {
border: 1px solid #d2e7b9;
background: var(--card-background-color);
}
.mdc-chip.entity:not(.add) .mdc-chip__icon--leading,
.mdc-chip.entity.add {
background: #d2e7b9;
}
.mdc-chip.label:not(.add) {
border: 1px solid var(--color, #e0e0e0);
background: var(--card-background-color);
}
.mdc-chip.label:not(.add) .mdc-chip__icon--leading,
.mdc-chip.label.add {
background: var(--background-color, #e0e0e0);
}
.mdc-chip:hover {
z-index: 5;
}
:host([disabled]) .mdc-chip {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
.tooltip-icon-img {
width: 24px;
height: 24px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-target-picker-chips-selection": HaTargetPickerChipsSelection;
}
}
@@ -0,0 +1,107 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker";
import type { HaEntityPickerEntityFilterFunc } from "../entity/ha-entity-picker";
import "../ha-expansion-panel";
import "../ha-md-list";
import "./ha-target-picker-item-row";
import type { TargetType } from "./ha-target-picker-item-row";
@customElement("ha-target-picker-item-group")
export class HaTargetPickerItemGroup extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public type!: "entity" | "device" | "area" | "label";
@property({ attribute: false }) public items!: Partial<
Record<TargetType, string[]>
>;
@property({ type: Boolean }) public collapsed = false;
@property({ attribute: false })
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property({ attribute: false })
public entityFilter?: HaEntityPickerEntityFilterFunc;
/**
* Show only targets with entities from specific domains.
* @type {Array}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show only targets with entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
protected render() {
let count = 0;
Object.values(this.items).forEach((items) => {
if (items) {
count += items.length;
}
});
return html`<ha-expansion-panel .expanded=${!this.collapsed} left-chevron>
<div slot="header" class="heading">
${this.hass.localize(
`ui.components.target-picker.selected.${this.type}`,
{
count,
}
)}
</div>
<ha-md-list>
${Object.entries(this.items).map(([type, items]) =>
items
? items.map(
(item) =>
html`<ha-target-picker-item-row
.hass=${this.hass}
.type=${type as "entity" | "device" | "area" | "label"}
.itemId=${item}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
></ha-target-picker-item-row>`
)
: nothing
)}
</ha-md-list>
</ha-expansion-panel>`;
}
static styles = css`
:host {
display: block;
--expansion-panel-content-padding: 0;
}
ha-expansion-panel::part(summary) {
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: 4px 8px;
font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color);
display: flex;
justify-content: space-between;
min-height: unset;
}
ha-md-list {
padding: 0;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-target-picker-item-group": HaTargetPickerItemGroup;
}
}
@@ -0,0 +1,699 @@
import { consume } from "@lit/context";
import {
mdiClose,
mdiDevices,
mdiHome,
mdiLabel,
mdiTextureBox,
} from "@mdi/js";
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeAreaName } from "../../common/entity/compute_area_name";
import {
computeDeviceName,
computeDeviceNameDisplay,
} from "../../common/entity/compute_device_name";
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 { getConfigEntry } from "../../data/config_entries";
import { labelsContext } from "../../data/context";
import { domainToName } from "../../data/integration";
import type { LabelRegistryEntry } from "../../data/label_registry";
import {
areaMeetsFilter,
deviceMeetsFilter,
entityRegMeetsFilter,
extractFromTarget,
type ExtractFromTargetResult,
type ExtractFromTargetResultReferenced,
} from "../../data/target";
import { buttonLinkStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker";
import type { HaEntityPickerEntityFilterFunc } from "../entity/ha-entity-picker";
import { floorDefaultIconPath } from "../ha-floor-icon";
import "../ha-icon-button";
import "../ha-md-list";
import type { HaMdList } from "../ha-md-list";
import "../ha-md-list-item";
import type { HaMdListItem } from "../ha-md-list-item";
import "../ha-state-icon";
import "../ha-svg-icon";
import { showTargetDetailsDialog } from "./dialog/show-dialog-target-details";
export type TargetType = "entity" | "device" | "area" | "label" | "floor";
@customElement("ha-target-picker-item-row")
export class HaTargetPickerItemRow extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ reflect: true }) public type!: TargetType;
@property({ attribute: "item-id" }) public itemId!: string;
@property({ type: Boolean }) public expand = false;
@property({ type: Boolean, attribute: "last" }) public lastItem = false;
@property({ type: Boolean, attribute: "sub-entry", reflect: true })
public subEntry = false;
@property({ type: Boolean, attribute: "hide-context" })
public hideContext = false;
@property({ attribute: false })
public parentEntries?: ExtractFromTargetResultReferenced;
@property({ attribute: false })
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property({ attribute: false })
public entityFilter?: HaEntityPickerEntityFilterFunc;
/**
* Show only targets with entities from specific domains.
* @type {Array}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show only targets with entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
@state() private _iconImg?: string;
@state() private _domainName?: string;
@state() private _entries?: ExtractFromTargetResult;
@state()
@consume({ context: labelsContext, subscribe: true })
_labelRegistry!: LabelRegistryEntry[];
@query("ha-md-list-item") public item?: HaMdListItem;
@query("ha-md-list") public list?: HaMdList;
@query("ha-target-picker-item-row") public itemRow?: HaTargetPickerItemRow;
protected willUpdate(changedProps: PropertyValues) {
if (!this.subEntry && changedProps.has("itemId")) {
this._updateItemData();
}
}
protected render() {
const { name, context, iconPath, fallbackIconPath, stateObject } =
this._itemData(this.type, this.itemId);
const showDevices = ["floor", "area", "label"].includes(this.type);
const showEntities = this.type !== "entity";
const entries = this.parentEntries || this._entries;
// Don't show sub entries that have no entities
if (
this.subEntry &&
this.type !== "entity" &&
(!entries || entries.referenced_entities.length === 0)
) {
return nothing;
}
return html`
<ha-md-list-item type="text">
<div slot="start">
${this.subEntry
? html`
<div class="horizontal-line-wrapper">
<div class="horizontal-line"></div>
</div>
`
: nothing}
${iconPath
? html`<ha-icon .icon=${iconPath}></ha-icon>`
: this._iconImg
? html`<img
alt=${this._domainName || ""}
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${this._iconImg}
/>`
: fallbackIconPath
? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
: stateObject
? html`
<ha-state-icon
.hass=${this.hass}
.stateObj=${stateObject}
>
</ha-state-icon>
`
: nothing}
</div>
<div slot="headline">${name}</div>
${context && !this.hideContext
? html`<span slot="supporting-text">${context}</span>`
: this._domainName && this.subEntry
? html`<span slot="supporting-text" class="domain"
>${this._domainName}</span
>`
: nothing}
${!this.subEntry &&
((entries && (showEntities || showDevices)) || this._domainName)
? html`
<div slot="end" class="summary">
${showEntities && !this.expand
? html`<button class="main link" @click=${this._openDetails}>
${this.hass.localize(
"ui.components.target-picker.entities_count",
{
count: entries?.referenced_entities.length,
}
)}
</button>`
: showEntities
? html`<span class="main">
${this.hass.localize(
"ui.components.target-picker.entities_count",
{
count: entries?.referenced_entities.length,
}
)}
</span>`
: nothing}
${showDevices
? html`<span class="secondary"
>${this.hass.localize(
"ui.components.target-picker.devices_count",
{
count: entries?.referenced_devices.length,
}
)}</span
>`
: nothing}
${this._domainName && !showDevices
? html`<span class="secondary domain"
>${this._domainName}</span
>`
: nothing}
</div>
`
: nothing}
${!this.expand && !this.subEntry
? html`
<ha-icon-button
.path=${mdiClose}
slot="end"
@click=${this._removeItem}
></ha-icon-button>
`
: nothing}
</ha-md-list-item>
${this.expand && entries && entries.referenced_entities
? this._renderEntries()
: nothing}
`;
}
private _renderEntries() {
const entries = this.parentEntries || this._entries;
let nextType: TargetType =
this.type === "floor"
? "area"
: this.type === "area"
? "device"
: "entity";
if (this.type === "label") {
if (entries?.referenced_areas.length) {
nextType = "area";
} else if (entries?.referenced_devices.length) {
nextType = "device";
}
}
const rows1 =
(nextType === "area"
? entries?.referenced_areas
: nextType === "device"
? entries?.referenced_devices
: entries?.referenced_entities) || [];
const devicesInAreas = [] as string[];
const rows1Entries =
nextType === "entity"
? undefined
: rows1.map((rowItem) => {
const nextEntries = {
referenced_areas: [] as string[],
referenced_devices: [] as string[],
referenced_entities: [] as string[],
};
if (nextType === "area") {
nextEntries.referenced_devices =
entries?.referenced_devices.filter(
(device_id) =>
this.hass.devices?.[device_id]?.area_id === rowItem &&
entries?.referenced_entities.some(
(entity_id) =>
this.hass.entities?.[entity_id]?.device_id === device_id
)
) || ([] as string[]);
devicesInAreas.push(...nextEntries.referenced_devices);
nextEntries.referenced_entities =
entries?.referenced_entities.filter((entity_id) => {
const entity = this.hass.entities[entity_id];
return (
entity.area_id === rowItem ||
!entity.device_id ||
nextEntries.referenced_devices.includes(entity.device_id)
);
}) || ([] as string[]);
return nextEntries;
}
nextEntries.referenced_entities =
entries?.referenced_entities.filter(
(entity_id) =>
this.hass.entities?.[entity_id]?.device_id === rowItem
) || ([] as string[]);
return nextEntries;
});
const entityRows =
this.type === "label" && entries
? entries.referenced_entities.filter((entity_id) =>
this.hass.entities[entity_id].labels.includes(this.itemId)
)
: nextType === "device" && entries
? entries.referenced_entities.filter(
(entity_id) =>
this.hass.entities[entity_id].area_id === this.itemId
)
: [];
const deviceRows =
this.type === "label" && entries
? entries.referenced_devices.filter(
(device_id) =>
!devicesInAreas.includes(device_id) &&
this.hass.devices[device_id].labels.includes(this.itemId)
)
: [];
const deviceRowsEntries =
deviceRows.length === 0
? undefined
: deviceRows.map((device_id) => ({
referenced_areas: [] as string[],
referenced_devices: [] as string[],
referenced_entities:
entries?.referenced_entities.filter(
(entity_id) =>
this.hass.entities?.[entity_id]?.device_id === device_id
) || ([] as string[]),
}));
return html`
<div class="entries-tree">
<div class="line-wrapper">
<div class="line"></div>
</div>
<ha-md-list class="entries">
${rows1.map(
(itemId, index) => html`
<ha-target-picker-item-row
sub-entry
.hass=${this.hass}
.type=${nextType}
.itemId=${itemId}
.parentEntries=${rows1Entries?.[index]}
.hideContext=${this.hideContext || this.type !== "label"}
expand
.lastItem=${deviceRows.length === 0 &&
entityRows.length === 0 &&
index === rows1.length - 1}
></ha-target-picker-item-row>
`
)}
${deviceRows.map(
(itemId, index) => html`
<ha-target-picker-item-row
sub-entry
.hass=${this.hass}
type="device"
.itemId=${itemId}
.parentEntries=${deviceRowsEntries?.[index]}
.hideContext=${this.hideContext || this.type !== "label"}
expand
.lastItem=${entityRows.length === 0 &&
index === deviceRows.length - 1}
></ha-target-picker-item-row>
`
)}
${entityRows.map(
(itemId, index) => html`
<ha-target-picker-item-row
sub-entry
.hass=${this.hass}
type="entity"
.itemId=${itemId}
.hideContext=${this.hideContext || this.type !== "label"}
.lastItem=${index === entityRows.length - 1}
></ha-target-picker-item-row>
`
)}
</ha-md-list>
</div>
`;
}
private async _updateItemData() {
if (this.type === "entity") {
this._entries = undefined;
return;
}
try {
const entries = await extractFromTarget(this.hass, {
[`${this.type}_id`]: [this.itemId],
});
const hiddenAreaIds: string[] = [];
if (this.type === "floor" || this.type === "label") {
entries.referenced_areas = entries.referenced_areas.filter(
(area_id) => {
const area = this.hass.areas[area_id];
if (
(this.type === "floor" || area.labels.includes(this.itemId)) &&
areaMeetsFilter(
area,
this.hass.devices,
this.hass.entities,
this.deviceFilter,
this.includeDomains,
this.includeDeviceClasses,
this.hass.states,
this.entityFilter
)
) {
return true;
}
hiddenAreaIds.push(area_id);
return false;
}
);
}
const hiddenDeviceIds: string[] = [];
if (
this.type === "floor" ||
this.type === "area" ||
this.type === "label"
) {
entries.referenced_devices = entries.referenced_devices.filter(
(device_id) => {
const device = this.hass.devices[device_id];
if (
!hiddenAreaIds.includes(device.area_id || "") &&
(this.type !== "label" || device.labels.includes(this.itemId)) &&
deviceMeetsFilter(
device,
this.hass.entities,
this.deviceFilter,
this.includeDomains,
this.includeDeviceClasses,
this.hass.states,
this.entityFilter
)
) {
return true;
}
hiddenDeviceIds.push(device_id);
return false;
}
);
}
entries.referenced_entities = entries.referenced_entities.filter(
(entity_id) => {
const entity = this.hass.entities[entity_id];
if (hiddenDeviceIds.includes(entity.device_id || "")) {
return false;
}
if (
(this.type === "area" && entity.area_id === this.itemId) ||
(this.type === "label" && entity.labels.includes(this.itemId)) ||
entries.referenced_devices.includes(entity.device_id || "")
) {
return entityRegMeetsFilter(
entity,
this.type === "label",
this.includeDomains,
this.includeDeviceClasses,
this.hass.states,
this.entityFilter
);
}
return false;
}
);
this._entries = entries;
} catch (e) {
// eslint-disable-next-line no-console
console.error("Failed to extract target", e);
}
}
private _itemData = memoizeOne((type: TargetType, item: string) => {
if (type === "floor") {
const floor = this.hass.floors?.[item];
return {
name: floor?.name || item,
iconPath: floor?.icon,
fallbackIconPath: floor ? floorDefaultIconPath(floor) : mdiHome,
};
}
if (type === "area") {
const area = this.hass.areas?.[item];
return {
name: area?.name || item,
context: area.floor_id && this.hass.floors?.[area.floor_id]?.name,
iconPath: area?.icon,
fallbackIconPath: mdiTextureBox,
};
}
if (type === "device") {
const device = this.hass.devices?.[item];
if (device.primary_config_entry) {
this._getDeviceDomain(device.primary_config_entry);
}
return {
name: device ? computeDeviceNameDisplay(device, this.hass) : item,
context: device?.area_id && this.hass.areas?.[device.area_id]?.name,
fallbackIconPath: mdiDevices,
};
}
if (type === "entity") {
this._setDomainName(computeDomain(item));
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]
.filter(Boolean)
.join(computeRTL(this.hass) ? " ◂ " : " ▸ ");
return {
name: entityName || deviceName || item,
context,
stateObject,
};
}
// type label
const label = this._labelRegistry.find((lab) => lab.label_id === item);
return {
name: label?.name || item,
iconPath: label?.icon,
fallbackIconPath: mdiLabel,
};
});
private _setDomainName(domain: string) {
this._domainName = domainToName(this.hass.localize, domain);
}
private _removeItem(ev) {
ev.stopPropagation();
fireEvent(this, "remove-target-item", {
type: this.type,
id: this.itemId,
});
}
private async _getDeviceDomain(configEntryId: string) {
try {
const data = await getConfigEntry(this.hass, configEntryId);
const domain = data.config_entry.domain;
this._iconImg = brandsUrl({
domain: domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
});
this._setDomainName(domain);
} catch {
// failed to load config entry -> ignore
}
}
private _openDetails() {
showTargetDetailsDialog(this, {
title: this._itemData(this.type, this.itemId).name,
type: this.type,
itemId: this.itemId,
deviceFilter: this.deviceFilter,
entityFilter: this.entityFilter,
includeDomains: this.includeDomains,
includeDeviceClasses: this.includeDeviceClasses,
});
}
static styles = [
buttonLinkStyle,
css`
:host {
--md-list-item-top-space: 0;
--md-list-item-bottom-space: 0;
--md-list-item-leading-space: 8px;
--md-list-item-trailing-space: 8px;
--md-list-item-two-line-container-height: 56px;
}
:host([expand]:not([sub-entry])) ha-md-list-item {
border: 2px solid var(--ha-color-border-neutral-loud);
background-color: var(--ha-color-fill-neutral-quiet-resting);
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
}
state-badge {
color: var(--ha-color-on-neutral-quiet);
}
img {
width: 24px;
height: 24px;
}
ha-icon-button {
--mdc-icon-button-size: 32px;
}
.summary {
display: flex;
flex-direction: column;
align-items: flex-end;
line-height: var(--ha-line-height-condensed);
}
:host([sub-entry]) .summary {
margin-right: 48px;
}
.summary .main {
font-weight: var(--ha-font-weight-medium);
}
.summary .secondary {
font-size: var(--ha-font-size-s);
color: var(--secondary-text-color);
}
.domain {
font-family: var(--ha-font-family-code);
}
.entries-tree {
display: flex;
position: relative;
}
.entries-tree .line-wrapper {
padding: 20px;
}
.entries-tree .line-wrapper .line {
border-left: 2px dashed var(--divider-color);
height: calc(100% - 28px);
position: absolute;
top: 0;
}
:host([sub-entry]) .entries-tree .line-wrapper .line {
height: calc(100% - 12px);
top: -18px;
}
.entries {
padding: 0;
--md-item-overflow: visible;
}
.horizontal-line-wrapper {
position: relative;
}
.horizontal-line-wrapper .horizontal-line {
position: absolute;
top: 11px;
margin-inline-start: -28px;
width: 29px;
border-top: 2px dashed var(--divider-color);
}
button.link {
text-decoration: none;
color: var(--primary-color);
}
button.link:hover,
button.link:focus {
text-decoration: underline;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-target-picker-item-row": HaTargetPickerItemRow;
}
}
+2 -2
View File
@@ -28,6 +28,6 @@ declare global {
}
}
export const forwardHaptic = (hapticType: HapticType) => {
fireEvent(window, "haptic", hapticType);
export const forwardHaptic = (node: HTMLElement, hapticType: HapticType) => {
fireEvent(node, "haptic", hapticType);
};
+161
View File
@@ -0,0 +1,161 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain";
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
import type { HaEntityPickerEntityFilterFunc } from "../components/entity/ha-entity-picker";
import type { HomeAssistant } from "../types";
import type { AreaRegistryEntry } from "./area_registry";
import type { DeviceRegistryEntry } from "./device_registry";
import type { EntityRegistryDisplayEntry } from "./entity_registry";
export interface ExtractFromTargetResult {
missing_areas: string[];
missing_devices: string[];
missing_floors: string[];
missing_labels: string[];
referenced_areas: string[];
referenced_devices: string[];
referenced_entities: string[];
}
export interface ExtractFromTargetResultReferenced {
referenced_areas: string[];
referenced_devices: string[];
referenced_entities: string[];
}
export const extractFromTarget = async (
hass: HomeAssistant,
target: HassServiceTarget
) =>
hass.callWS<ExtractFromTargetResult>({
type: "extract_from_target",
target,
});
export const areaMeetsFilter = (
area: AreaRegistryEntry,
devices: HomeAssistant["devices"],
entities: HomeAssistant["entities"],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
includeDomains?: string[],
includeDeviceClasses?: string[],
states?: HomeAssistant["states"],
entityFilter?: HaEntityPickerEntityFilterFunc
): boolean => {
const areaDevices = Object.values(devices).filter(
(device) => device.area_id === area.area_id
);
if (
areaDevices.some((device) =>
deviceMeetsFilter(
device,
entities,
deviceFilter,
includeDomains,
includeDeviceClasses,
states,
entityFilter
)
)
) {
return true;
}
const areaEntities = Object.values(entities).filter(
(entity) => entity.area_id === area.area_id
);
if (
areaEntities.some((entity) =>
entityRegMeetsFilter(
entity,
false,
includeDomains,
includeDeviceClasses,
states,
entityFilter
)
)
) {
return true;
}
return false;
};
export const deviceMeetsFilter = (
device: DeviceRegistryEntry,
entities: HomeAssistant["entities"],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
includeDomains?: string[],
includeDeviceClasses?: string[],
states?: HomeAssistant["states"],
entityFilter?: HaEntityPickerEntityFilterFunc
): boolean => {
const devEntities = Object.values(entities).filter(
(entity) => entity.device_id === device.id
);
if (
!devEntities.some((entity) =>
entityRegMeetsFilter(
entity,
false,
includeDomains,
includeDeviceClasses,
states,
entityFilter
)
)
) {
return false;
}
if (deviceFilter) {
return deviceFilter(device);
}
return true;
};
export const entityRegMeetsFilter = (
entity: EntityRegistryDisplayEntry,
includeSecondary = false,
includeDomains?: string[],
includeDeviceClasses?: string[],
states?: HomeAssistant["states"],
entityFilter?: HaEntityPickerEntityFilterFunc
): boolean => {
if (entity.hidden || (entity.entity_category && !includeSecondary)) {
return false;
}
if (
includeDomains &&
!includeDomains.includes(computeDomain(entity.entity_id))
) {
return false;
}
if (includeDeviceClasses) {
const stateObj = states?.[entity.entity_id];
if (!stateObj) {
return false;
}
if (
!stateObj.attributes.device_class ||
!includeDeviceClasses!.includes(stateObj.attributes.device_class)
) {
return false;
}
}
if (entityFilter) {
const stateObj = states?.[entity.entity_id];
if (!stateObj) {
return false;
}
return entityFilter!(stateObj);
}
return true;
};
@@ -42,7 +42,7 @@ class MoreInfoFan extends LitElement {
private _toggle = () => {
const service = this.stateObj?.state === "on" ? "turn_off" : "turn_on";
forwardHaptic("light");
forwardHaptic(this, "light");
this.hass.callService("fan", service, {
entity_id: this.stateObj!.entity_id,
});
@@ -310,7 +310,7 @@ class MoreInfoLight extends LitElement {
private _toggle = () => {
const service = this.stateObj?.state === "on" ? "turn_off" : "turn_on";
forwardHaptic("light");
forwardHaptic(this, "light");
this.hass.callService("light", service, {
entity_id: this.stateObj!.entity_id,
});
-3
View File
@@ -32,9 +32,6 @@ const COMPONENTS = {
todo: () => import("../panels/todo/ha-panel-todo"),
"media-browser": () =>
import("../panels/media-browser/ha-panel-media-browser"),
lights: () => import("../panels/lights/ha-panel-lights"),
security: () => import("../panels/security/ha-panel-security"),
climate: () => import("../panels/climate/ha-panel-climate"),
};
@customElement("partial-panel-resolver")
-214
View File
@@ -1,214 +0,0 @@
import { mdiCog } from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { goBack, navigate } from "../../common/navigate";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-menu-button";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
const CLIMATE_LOVELACE_CONFIG: LovelaceConfig = {
views: [
{
strategy: {
type: "climate",
},
},
],
};
@customElement("ha-panel-climate")
class PanelClimate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@state() private _viewIndex = 0;
@state() private _lovelace?: Lovelace;
@state() private _searchParms = new URLSearchParams(window.location.search);
public firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
}
public willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
}
if (!changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as this["hass"];
if (oldHass?.locale !== this.hass.locale) {
this._setLovelace();
}
}
private _back(ev) {
ev.stopPropagation();
goBack();
}
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.climate")}</div>
${this.hass.user?.is_admin
? html`<ha-icon-button
@click=${this._navigateConfig}
.path=${mdiCog}
title=${this.hass!.localize("ui.panel.energy.configure")}
>
</ha-icon-button>`
: nothing}
</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 _setLovelace() {
this._lovelace = {
config: CLIMATE_LOVELACE_CONFIG,
rawConfig: CLIMATE_LOVELACE_CONFIG,
editMode: false,
urlPath: "climate",
mode: "generated",
locale: this.hass.locale,
enableFullEditMode: () => undefined,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
showToast: () => undefined,
};
}
private _navigateConfig(ev) {
ev.stopPropagation();
navigate("/config/climate?historyBack=1");
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
.header {
background-color: var(--app-header-background-color);
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
position: fixed;
top: 0;
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-right,
0px
)
);
padding-top: var(--safe-area-inset-top);
z-index: 4;
transition: box-shadow 200ms linear;
display: flex;
flex-direction: row;
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
backdrop-filter: var(--app-header-backdrop-filter, none);
padding-top: var(--safe-area-inset-top);
padding-right: var(--safe-area-inset-right);
}
:host([narrow]) .header {
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-left,
0px
) - var(--safe-area-inset-right, 0px)
);
padding-left: var(--safe-area-inset-left);
}
:host([scrolled]) .header {
box-shadow: var(
--mdc-top-app-bar-fixed-box-shadow,
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
);
}
.toolbar {
height: var(--header-height);
display: flex;
flex: 1;
align-items: center;
font-size: var(--ha-font-size-xl);
padding: 0px 12px;
font-weight: var(--ha-font-weight-normal);
box-sizing: border-box;
}
:host([narrow]) .toolbar {
padding: 0 4px;
}
.main-title {
margin: var(--margin-title);
line-height: var(--ha-line-height-normal);
flex-grow: 1;
}
hui-view-container {
position: relative;
display: flex;
min-height: 100vh;
box-sizing: border-box;
padding-top: calc(var(--header-height) + var(--safe-area-inset-top));
padding-right: var(--safe-area-inset-right);
padding-inline-end: var(--safe-area-inset-right);
padding-bottom: var(--safe-area-inset-bottom);
}
:host([narrow]) hui-view-container {
padding-left: var(--safe-area-inset-left);
padding-inline-start: var(--safe-area-inset-left);
}
hui-view {
flex: 1 1 100%;
max-width: 100%;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-panel-climate": PanelClimate;
}
}
@@ -1,203 +0,0 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import {
findEntities,
generateEntityFilter,
type EntityFilter,
} from "../../../common/entity/entity_filter";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import {
computeAreaTileCardConfig,
getAreas,
getFloors,
} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure";
export interface ClimateViewStrategyConfig {
type: "climate";
}
export const climateEntityFilters: EntityFilter[] = [
{ domain: "climate", entity_category: "none" },
{ domain: "humidifier", entity_category: "none" },
{ domain: "fan", entity_category: "none" },
{ domain: "water_heater", entity_category: "none" },
{
domain: "cover",
device_class: [
"awning",
"blind",
"curtain",
"shade",
"shutter",
"window",
"none",
],
entity_category: "none",
},
{
domain: "binary_sensor",
device_class: ["window"],
entity_category: "none",
},
];
const processAreasForClimate = (
areaIds: string[],
hass: HomeAssistant,
entities: string[]
): LovelaceCardConfig[] => {
const cards: LovelaceCardConfig[] = [];
const computeTileCard = computeAreaTileCardConfig(hass, "", true);
for (const areaId of areaIds) {
const area = hass.areas[areaId];
if (!area) continue;
const areaFilter = generateEntityFilter(hass, {
area: area.area_id,
});
const areaClimateEntities = entities.filter(areaFilter);
const areaCards: LovelaceCardConfig[] = [];
// Add temperature and humidity sensors with trend graphs for areas
const temperatureEntityId = area.temperature_entity_id;
if (temperatureEntityId && hass.states[temperatureEntityId]) {
areaCards.push({
...computeTileCard(temperatureEntityId),
features: [{ type: "trend-graph" }],
});
}
const humidityEntityId = area.humidity_entity_id;
if (humidityEntityId && hass.states[humidityEntityId]) {
areaCards.push({
...computeTileCard(humidityEntityId),
features: [{ type: "trend-graph" }],
});
}
// Add other climate entities
for (const entityId of areaClimateEntities) {
// Skip if already added as temperature/humidity sensor
if (entityId === temperatureEntityId || entityId === humidityEntityId) {
continue;
}
const state = hass.states[entityId];
if (
state?.attributes.device_class === "temperature" ||
state?.attributes.device_class === "humidity"
) {
areaCards.push({
...computeTileCard(entityId),
features: [{ type: "trend-graph" }],
});
} else {
areaCards.push(computeTileCard(entityId));
}
}
if (areaCards.length > 0) {
cards.push({
heading_style: "subtitle",
type: "heading",
heading: area.name,
});
cards.push(...areaCards);
}
}
return cards;
};
@customElement("climate-view-strategy")
export class ClimateViewStrategy extends ReactiveElement {
static async generate(
_config: ClimateViewStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const areas = getAreas(hass.areas);
const floors = getFloors(hass.floors);
const home = getHomeStructure(floors, areas);
const sections: LovelaceSectionRawConfig[] = [];
const allEntities = Object.keys(hass.states);
const climateFilters = climateEntityFilters.map((filter) =>
generateEntityFilter(hass, filter)
);
const entities = findEntities(allEntities, climateFilters);
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
// Process floors
for (const floorStructure of home.floors) {
const floorId = floorStructure.id;
const areaIds = floorStructure.areas;
const floor = hass.floors[floorId];
const section: LovelaceSectionRawConfig = {
type: "grid",
column_span: 2,
cards: [
{
type: "heading",
heading:
floorCount > 1
? floor.name
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
},
],
};
const areaCards = processAreasForClimate(areaIds, hass, entities);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);
sections.push(section);
}
}
// Process unassigned areas
if (home.areas.length > 0) {
const section: LovelaceSectionRawConfig = {
type: "grid",
column_span: 2,
cards: [
{
type: "heading",
heading:
floorCount > 1
? hass.localize("ui.panel.lovelace.strategy.home.other_areas")
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
},
],
};
const areaCards = processAreasForClimate(home.areas, hass, entities);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);
sections.push(section);
}
}
return {
type: "sections",
max_columns: 2,
sections: sections || [],
};
}
}
declare global {
interface HTMLElementTagNameMap {
"climate-view-strategy": ClimateViewStrategy;
}
}
@@ -1,42 +0,0 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types";
@customElement("ha-config-climate")
class HaConfigClimate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@state() private _searchParms = new URLSearchParams(window.location.search);
protected render(): TemplateResult {
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config/lovelace/dashboards"}
.header=${"Climate"}
>
<div class="container"></div>
</hass-subpage>
`;
}
static get styles(): CSSResultGroup {
return [haStyle, css``];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-climate": HaConfigClimate;
}
}
-4
View File
@@ -577,10 +577,6 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
load: () =>
import("./application_credentials/ha-config-application-credentials"),
},
climate: {
tag: "ha-config-climate",
load: () => import("./climate/ha-config-climate"),
},
},
};
@@ -210,10 +210,10 @@ export class ZHAClusterAttributes extends LitElement {
this._readingAttribute = true;
try {
this._attributeValue = await readAttributeValue(this.hass, data);
forwardHaptic("success");
forwardHaptic(this, "success");
button.actionSuccess();
} catch (_err: any) {
forwardHaptic("failure");
forwardHaptic(this, "failure");
button.actionError();
} finally {
this._readingAttribute = false;
+9 -7
View File
@@ -10,6 +10,7 @@ import type { PropertyValues } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor } from "../../../common/color/compute-color";
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
import { storage } from "../../../common/decorators/storage";
@@ -104,13 +105,14 @@ export class HaConfigLabels extends LitElement {
template: (label) =>
label.color
? html`<div
style="
background-color: ${computeCssColor(label.color)};
border-radius: 10px;
border: 1px solid var(--outline-color);
box-sizing: border-box;
width: 20px;
height: 20px;"
style=${styleMap({
backgroundColor: computeCssColor(label.color),
borderRadius: "10px",
border: "1px solid var(--outline-color)",
boxSizing: "border-box",
width: "20px",
height: "20px",
})}
></div>`
: nothing,
},
@@ -238,17 +238,13 @@ export class HaConfigLovelaceDashboards extends LitElement {
.hass=${this.hass}
narrow
.items=${[
...(this._canEdit(dashboard.url_path)
? [
{
path: mdiPencil,
label: this.hass.localize(
"ui.panel.config.lovelace.dashboards.picker.edit"
),
action: () => this._handleEdit(dashboard),
},
]
: []),
{
path: mdiPencil,
label: this.hass.localize(
"ui.panel.config.lovelace.dashboards.picker.edit"
),
action: () => this._handleEdit(dashboard),
},
...(this._canDelete(dashboard.url_path)
? [
{
@@ -298,49 +294,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
mode: "storage",
url_path: "energy",
filename: "",
iconColor: "var(--orange-color)",
default: false,
require_admin: false,
});
}
if (this.hass.panels.lights) {
result.push({
icon: "mdi:lamps",
title: this.hass.localize("panel.lights"),
show_in_sidebar: false,
mode: "storage",
url_path: "lights",
filename: "",
iconColor: "var(--amber-color)",
default: false,
require_admin: false,
});
}
if (this.hass.panels.security) {
result.push({
icon: "mdi:security",
title: this.hass.localize("panel.security"),
show_in_sidebar: false,
mode: "storage",
url_path: "security",
filename: "",
iconColor: "var(--blue-grey-color)",
default: false,
require_admin: false,
});
}
if (this.hass.panels.climate) {
result.push({
icon: "mdi:home-thermometer",
title: this.hass.localize("panel.climate"),
show_in_sidebar: false,
mode: "storage",
url_path: "climate",
filename: "",
iconColor: "var(--deep-orange-color)",
iconColor: "var(--label-badge-yellow)",
default: false,
require_admin: false,
});
@@ -438,28 +392,12 @@ export class HaConfigLovelaceDashboards extends LitElement {
navigate("/config/energy");
return;
}
if (urlPath === "climate") {
navigate("/config/climate");
return;
}
const dashboard = this._dashboards.find((res) => res.url_path === urlPath);
this._openDetailDialog(dashboard, urlPath);
}
private _canDelete(urlPath: string) {
if (
urlPath === "lovelace" ||
urlPath === "energy" ||
urlPath === "lights" ||
urlPath === "security"
) {
return false;
}
return true;
}
private _canEdit(urlPath: string) {
if (urlPath === "lights" || urlPath === "security") {
if (urlPath === "lovelace" || urlPath === "energy") {
return false;
}
return true;
@@ -1087,7 +1087,7 @@ ${rejected
name: computeStateName(scene),
}),
});
forwardHaptic("light");
forwardHaptic(this, "light");
};
private _deleteConfirm(scene: SceneEntity): void {
@@ -443,7 +443,7 @@ class HaPanelDevAction extends LitElement {
const button = ev.currentTarget as HaProgressButton;
if (this._yamlMode && !this._yamlValid) {
forwardHaptic("failure");
forwardHaptic(this, "failure");
button.actionError();
this._error = this.hass.localize(
"ui.panel.developer-tools.tabs.actions.errors.yaml.invalid_yaml"
@@ -465,7 +465,7 @@ class HaPanelDevAction extends LitElement {
);
if (this._error !== undefined) {
forwardHaptic("failure");
forwardHaptic(this, "failure");
button.actionError();
return;
}
@@ -534,7 +534,7 @@ class HaPanelDevAction extends LitElement {
) {
return;
}
forwardHaptic("failure");
forwardHaptic(this, "failure");
button.actionError();
let localizedErrorMessage: string | undefined;
+25 -7
View File
@@ -1,10 +1,11 @@
import { ContextProvider } from "@lit/context";
import type { ActionDetail } from "@material/mwc-list";
import {
mdiDotsVertical,
mdiDownload,
mdiFilterRemove,
mdiImagePlus,
} from "@mdi/js";
import type { ActionDetail } from "@material/mwc-list";
import { differenceInHours } from "date-fns";
import type {
HassServiceTarget,
@@ -27,32 +28,35 @@ import {
import { MIN_TIME_BETWEEN_UPDATES } from "../../components/chart/ha-chart-base";
import "../../components/chart/state-history-charts";
import type { StateHistoryCharts } from "../../components/chart/state-history-charts";
import "../../components/ha-spinner";
import "../../components/ha-button-menu";
import "../../components/ha-date-range-picker";
import "../../components/ha-icon-button";
import "../../components/ha-button-menu";
import "../../components/ha-list-item";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-list-item";
import "../../components/ha-menu-button";
import "../../components/ha-spinner";
import "../../components/ha-target-picker";
import "../../components/ha-top-app-bar-fixed";
import { labelsContext } from "../../data/context";
import type { HistoryResult } from "../../data/history";
import {
computeHistory,
subscribeHistory,
mergeHistoryResults,
convertStatisticsToHistory,
mergeHistoryResults,
subscribeHistory,
} from "../../data/history";
import { subscribeLabelRegistry } from "../../data/label_registry";
import { fetchStatistics } from "../../data/recorder";
import { resolveEntityIDs } from "../../data/selector";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { fileDownload } from "../../util/file_download";
import { addEntitiesToLovelaceView } from "../lovelace/editor/add-entities-to-view";
class HaPanelHistory extends LitElement {
class HaPanelHistory extends SubscribeMixin(LitElement) {
@property({ attribute: false }) hass!: HomeAssistant;
@property({ reflect: true, type: Boolean }) public narrow = false;
@@ -89,6 +93,11 @@ class HaPanelHistory extends LitElement {
private _interval?: number;
private _labelsContext = new ContextProvider(this, {
context: labelsContext,
initialValue: [],
});
public constructor() {
super();
@@ -108,6 +117,14 @@ class HaPanelHistory extends LitElement {
}
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeLabelRegistry(this.hass.connection!, (labels) => {
this._labelsContext.setValue(labels);
}),
];
}
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubscribeHistory();
@@ -182,6 +199,7 @@ class HaPanelHistory extends LitElement {
.disabled=${this._isLoading}
add-on-top
@value-changed=${this._targetsChanged}
compact
></ha-target-picker>
</div>
${this._isLoading
-200
View File
@@ -1,200 +0,0 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { goBack } from "../../common/navigate";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-menu-button";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
const LIGHTS_LOVELACE_CONFIG: LovelaceConfig = {
views: [
{
strategy: {
type: "lights",
},
},
],
};
@customElement("ha-panel-lights")
class PanelLights extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@state() private _viewIndex = 0;
@state() private _lovelace?: Lovelace;
@state() private _searchParms = new URLSearchParams(window.location.search);
public willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
}
if (!changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as this["hass"];
if (oldHass?.locale !== this.hass.locale) {
this._setLovelace();
}
}
private _back(ev) {
ev.stopPropagation();
goBack();
}
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.narrow
? html`<div class="main-title">
${this.hass.localize("panel.lights")}
</div>`
: nothing}
</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 _setLovelace() {
this._lovelace = {
config: LIGHTS_LOVELACE_CONFIG,
rawConfig: LIGHTS_LOVELACE_CONFIG,
editMode: false,
urlPath: "lights",
mode: "generated",
locale: this.hass.locale,
enableFullEditMode: () => undefined,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
showToast: () => undefined,
};
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
.header {
background-color: var(--app-header-background-color);
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
position: fixed;
top: 0;
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-right,
0px
)
);
padding-top: var(--safe-area-inset-top);
z-index: 4;
transition: box-shadow 200ms linear;
display: flex;
flex-direction: row;
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
backdrop-filter: var(--app-header-backdrop-filter, none);
padding-top: var(--safe-area-inset-top);
padding-right: var(--safe-area-inset-right);
}
:host([narrow]) .header {
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-left,
0px
) - var(--safe-area-inset-right, 0px)
);
padding-left: var(--safe-area-inset-left);
}
:host([scrolled]) .header {
box-shadow: var(
--mdc-top-app-bar-fixed-box-shadow,
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
);
}
.toolbar {
height: var(--header-height);
display: flex;
flex: 1;
align-items: center;
font-size: var(--ha-font-size-xl);
padding: 0px 12px;
font-weight: var(--ha-font-weight-normal);
box-sizing: border-box;
}
:host([narrow]) .toolbar {
padding: 0 4px;
}
.main-title {
margin: var(--margin-title);
line-height: var(--ha-line-height-normal);
flex-grow: 1;
}
hui-view-container {
position: relative;
display: flex;
min-height: 100vh;
box-sizing: border-box;
padding-top: calc(var(--header-height) + var(--safe-area-inset-top));
padding-right: var(--safe-area-inset-right);
padding-inline-end: var(--safe-area-inset-right);
padding-bottom: var(--safe-area-inset-bottom);
}
:host([narrow]) hui-view-container {
padding-left: var(--safe-area-inset-left);
padding-inline-start: var(--safe-area-inset-left);
}
hui-view {
flex: 1 1 100%;
max-width: 100%;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-panel-lights": PanelLights;
}
}
+29 -8
View File
@@ -1,9 +1,15 @@
import { ContextProvider } from "@lit/context";
import { mdiRefresh } from "@mdi/js";
import type {
HassServiceTarget,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { HassServiceTarget } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { storage } from "../../common/decorators/storage";
import { goBack, navigate } from "../../common/navigate";
import { constructUrlCurrentPath } from "../../common/url/construct-url";
import {
@@ -12,24 +18,25 @@ import {
removeSearchParam,
} from "../../common/url/search-params";
import "../../components/entity/ha-entity-picker";
import type { HaEntityPickerEntityFilterFunc } from "../../components/entity/ha-entity-picker";
import "../../components/ha-date-range-picker";
import "../../components/ha-icon-button";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-menu-button";
import "../../components/ha-top-app-bar-fixed";
import "../../components/ha-target-picker";
import "../../components/ha-top-app-bar-fixed";
import { labelsContext } from "../../data/context";
import { subscribeLabelRegistry } from "../../data/label_registry";
import { filterLogbookCompatibleEntities } from "../../data/logbook";
import { resolveEntityIDs } from "../../data/selector";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "./ha-logbook";
import { storage } from "../../common/decorators/storage";
import { ensureArray } from "../../common/array/ensure-array";
import { resolveEntityIDs } from "../../data/selector";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
import type { HaEntityPickerEntityFilterFunc } from "../../components/entity/ha-entity-picker";
@customElement("ha-panel-logbook")
export class HaPanelLogbook extends LitElement {
export class HaPanelLogbook extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@@ -51,6 +58,11 @@ export class HaPanelLogbook extends LitElement {
@state() private _sensorNumericDeviceClasses?: string[] = [];
private _labelsContext = new ContextProvider(this, {
context: labelsContext,
initialValue: [],
});
public constructor() {
super();
@@ -63,6 +75,14 @@ export class HaPanelLogbook extends LitElement {
this._time = { range: [start, end] };
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeLabelRegistry(this.hass.connection!, (labels) => {
this._labelsContext.setValue(labels);
}),
];
}
private _goBack(): void {
goBack();
}
@@ -108,6 +128,7 @@ export class HaPanelLogbook extends LitElement {
.value=${this._targetPickerValue}
add-on-top
@value-changed=${this._targetsChanged}
compact
></ha-target-picker>
</div>
@@ -177,7 +177,7 @@ class HuiAreaControlsCardFeature
.map((entityId) => this.hass!.states[entityId] as HassEntity | undefined)
.filter((v): v is HassEntity => Boolean(v));
forwardHaptic("light");
forwardHaptic(this, "light");
toggleGroupEntities(this.hass, entities);
}
@@ -69,7 +69,7 @@ class HuiLockCommandsCardFeature
if (!this.hass || !this._stateObj || !service) {
return;
}
forwardHaptic("light");
forwardHaptic(this, "light");
callProtectedLockService(this, this.hass, this._stateObj, service);
}
@@ -112,7 +112,7 @@ class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature {
if (!this.hass || !this._stateObj) {
return;
}
forwardHaptic("light");
forwardHaptic(this, "light");
const stateDomain = computeDomain(this._stateObj.entity_id);
const serviceDomain = stateDomain;
const service = turnOn ? "turn_on" : "turn_off";
@@ -5,10 +5,7 @@ import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor } from "../../../common/color/compute-color";
import { computeDomain } from "../../../common/entity/compute_domain";
import {
findEntities,
generateEntityFilter,
} from "../../../common/entity/entity_filter";
import { generateEntityFilter } from "../../../common/entity/entity_filter";
import { formatNumber } from "../../../common/number/format_number";
import "../../../components/ha-card";
import "../../../components/ha-icon";
@@ -22,6 +19,7 @@ import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import {
findEntities,
getSummaryLabel,
HOME_SUMMARIES_FILTERS,
HOME_SUMMARIES_ICONS,
@@ -231,6 +231,7 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
static styles = css`
ha-card {
height: 100%;
overflow-y: auto;
}
ha-alert {
margin-bottom: 8px;
+8 -8
View File
@@ -53,7 +53,7 @@ export const handleAction = async (
(e) => e.user === hass!.user?.id
))
) {
forwardHaptic("warning");
forwardHaptic(node, "warning");
let serviceName;
if (
@@ -107,7 +107,7 @@ export const handleAction = async (
"ui.panel.lovelace.cards.actions.no_entity_more_info"
),
});
forwardHaptic("failure");
forwardHaptic(node, "failure");
}
break;
}
@@ -122,7 +122,7 @@ export const handleAction = async (
"ui.panel.lovelace.cards.actions.no_navigation_path"
),
});
forwardHaptic("failure");
forwardHaptic(node, "failure");
}
break;
case "url": {
@@ -132,21 +132,21 @@ export const handleAction = async (
showToast(node, {
message: hass.localize("ui.panel.lovelace.cards.actions.no_url"),
});
forwardHaptic("failure");
forwardHaptic(node, "failure");
}
break;
}
case "toggle": {
if (config.entity) {
toggleEntity(hass, config.entity!);
forwardHaptic("light");
forwardHaptic(node, "light");
} else {
showToast(node, {
message: hass.localize(
"ui.panel.lovelace.cards.actions.no_entity_toggle"
),
});
forwardHaptic("failure");
forwardHaptic(node, "failure");
}
break;
}
@@ -156,7 +156,7 @@ export const handleAction = async (
showToast(node, {
message: hass.localize("ui.panel.lovelace.cards.actions.no_action"),
});
forwardHaptic("failure");
forwardHaptic(node, "failure");
return;
}
const [domain, service] = (actionConfig.perform_action ||
@@ -167,7 +167,7 @@ export const handleAction = async (
actionConfig.data ?? actionConfig.service_data,
actionConfig.target
);
forwardHaptic("light");
forwardHaptic(node, "light");
break;
}
case "assist": {
@@ -58,7 +58,7 @@ class HuiEntitiesToggle extends LitElement {
`;
private _callService(ev: MouseEvent): void {
forwardHaptic("light");
forwardHaptic(this, "light");
const turnOn = (ev.target as HaSwitch).checked;
turnOnOffEntities(this.hass!, this._toggleEntities!, turnOn!);
}
@@ -103,7 +103,7 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow {
return;
}
forwardHaptic("light");
forwardHaptic(this, "light");
setInputSelectOption(this.hass!, stateObj.entity_id, option);
}
@@ -104,7 +104,7 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow {
return;
}
forwardHaptic("light");
forwardHaptic(this, "light");
setSelectOption(this.hass!, stateObj.entity_id, option);
}
@@ -44,15 +44,12 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
area: () => import("./areas/area-view-strategy"),
"areas-overview": () => import("./areas/areas-overview-view-strategy"),
"home-main": () => import("./home/home-main-view-strategy"),
"home-lights": () => import("./home/home-lights-view-strategy"),
"home-climate": () => import("./home/home-climate-view-strategy"),
"home-security": () => import("./home/home-security-view-strategy"),
"home-media-players": () =>
import("./home/home-media-players-view-strategy"),
"home-area": () => import("./home/home-area-view-strategy"),
lights: () => import("../../lights/strategies/lights-view-strategy"),
security: () => import("../../security/strategies/security-view-strategy"),
climate: () => import("../../climate/strategies/climate-view-strategy"),
},
section: {
"common-controls": () =>
@@ -6,6 +6,7 @@ interface HomeStructure {
id: string;
areas: string[];
}[];
areas: string[];
}
@@ -1,8 +1,8 @@
import type { EntityFilter } from "../../../../../common/entity/entity_filter";
import type {
EntityFilter,
EntityFilterFunc,
} from "../../../../../common/entity/entity_filter";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import { climateEntityFilters } from "../../../../climate/strategies/climate-view-strategy";
import { lightEntityFilters } from "../../../../lights/strategies/lights-view-strategy";
import { securityEntityFilters } from "../../../../security/strategies/security-view-strategy";
export const HOME_SUMMARIES = [
"lights",
@@ -21,18 +21,97 @@ export const HOME_SUMMARIES_ICONS: Record<HomeSummary, string> = {
};
export const HOME_SUMMARIES_FILTERS: Record<HomeSummary, EntityFilter[]> = {
lights: lightEntityFilters,
climate: climateEntityFilters,
security: securityEntityFilters,
lights: [{ domain: "light", entity_category: "none" }],
climate: [
{ domain: "climate", entity_category: "none" },
{ domain: "humidifier", entity_category: "none" },
{ domain: "fan", entity_category: "none" },
{ domain: "water_heater", entity_category: "none" },
{
domain: "cover",
device_class: [
"awning",
"blind",
"curtain",
"shade",
"shutter",
"window",
"none",
],
entity_category: "none",
},
{
domain: "binary_sensor",
device_class: ["window"],
entity_category: "none",
},
],
security: [
{
domain: "camera",
entity_category: "none",
},
{
domain: "alarm_control_panel",
entity_category: "none",
},
{
domain: "lock",
entity_category: "none",
},
{
domain: "cover",
device_class: ["door", "garage", "gate"],
entity_category: "none",
},
{
domain: "binary_sensor",
device_class: [
// Locks
"lock",
// Openings
"door",
"window",
"garage_door",
"opening",
// Safety
"carbon_monoxide",
"gas",
"moisture",
"safety",
"smoke",
"tamper",
],
entity_category: "none",
},
// We also want the tamper sensors when they are diagnostic
{
domain: "binary_sensor",
device_class: ["tamper"],
entity_category: "diagnostic",
},
],
media_players: [{ domain: "media_player", entity_category: "none" }],
};
export const getSummaryLabel = (
localize: LocalizeFunc,
summary: HomeSummary
) => {
if (summary === "lights" || summary === "climate" || summary === "security") {
return localize(`panel.${summary}`);
export const findEntities = (
entities: string[],
filters: EntityFilterFunc[]
): string[] => {
const seen = new Set<string>();
const results: string[] = [];
for (const filter of filters) {
for (const entity of entities) {
if (filter(entity) && !seen.has(entity)) {
seen.add(entity);
results.push(entity);
}
}
}
return localize(`ui.panel.lovelace.strategy.home.summary_list.${summary}`);
return results;
};
export const getSummaryLabel = (localize: LocalizeFunc, summary: HomeSummary) =>
localize(`ui.panel.lovelace.strategy.home.summary_list.${summary}`);
@@ -2,10 +2,7 @@ import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
import { getEntityContext } from "../../../../common/entity/context/get_entity_context";
import {
findEntities,
generateEntityFilter,
} from "../../../../common/entity/entity_filter";
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
import { clamp } from "../../../../common/number/clamp";
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
@@ -15,6 +12,7 @@ import type { HomeAssistant } from "../../../../types";
import type { HeadingCardConfig } from "../../cards/types";
import { computeAreaTileCardConfig } from "../areas/helpers/areas-strategy-helper";
import {
findEntities,
getSummaryLabel,
HOME_SUMMARIES,
HOME_SUMMARIES_FILTERS,
@@ -115,7 +113,7 @@ export class HomeAreaViewStrategy extends ReactiveElement {
computeHeadingCard(
getSummaryLabel(hass.localize, "lights"),
HOME_SUMMARIES_ICONS.lights,
"/lights?historyBack=1"
"lights"
),
...lights.map(computeTileCard),
],
@@ -1,9 +1,6 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import {
findEntities,
generateEntityFilter,
} from "../../../../common/entity/entity_filter";
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
@@ -14,7 +11,7 @@ import {
getFloors,
} from "../areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "./helpers/home-structure";
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
import { findEntities, HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
export interface HomeClimateViewStrategyConfig {
type: "home-climate";
@@ -62,6 +62,16 @@ export class HomeDashboardStrategy extends ReactiveElement {
};
});
const lightView = {
title: getSummaryLabel(hass.localize, "lights"),
path: "lights",
subview: true,
strategy: {
type: "home-lights",
},
icon: HOME_SUMMARIES_ICONS.lights,
} satisfies LovelaceViewRawConfig;
const climateView = {
title: getSummaryLabel(hass.localize, "climate"),
path: "climate",
@@ -103,6 +113,7 @@ export class HomeDashboardStrategy extends ReactiveElement {
} satisfies HomeMainViewStrategyConfig,
},
...areaViews,
lightView,
climateView,
securityView,
mediaPlayersView,
@@ -1,29 +1,22 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import {
findEntities,
generateEntityFilter,
type EntityFilter,
} from "../../../common/entity/entity_filter";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import {
computeAreaTileCardConfig,
getAreas,
getFloors,
} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure";
} from "../areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "./helpers/home-structure";
import { findEntities, HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
export interface LightsViewStrategyConfig {
type: "lights";
export interface HomeLightsViewStrategyConfig {
type: "home-lights";
}
export const lightEntityFilters: EntityFilter[] = [
{ domain: "light", entity_category: "none" },
];
const processAreasForLights = (
areaIds: string[],
hass: HomeAssistant,
@@ -52,6 +45,10 @@ const processAreasForLights = (
heading_style: "subtitle",
type: "heading",
heading: area.name,
tap_action: {
action: "navigate",
navigation_path: `areas-${area.area_id}`,
},
});
cards.push(...areaCards);
}
@@ -60,10 +57,10 @@ const processAreasForLights = (
return cards;
};
@customElement("lights-view-strategy")
export class LightsViewStrategy extends ReactiveElement {
@customElement("home-lights-view-strategy")
export class HomeLightsViewStrategy extends ReactiveElement {
static async generate(
_config: LightsViewStrategyConfig,
_config: HomeLightsViewStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const areas = getAreas(hass.areas);
@@ -74,7 +71,7 @@ export class LightsViewStrategy extends ReactiveElement {
const allEntities = Object.keys(hass.states);
const lightsFilters = lightEntityFilters.map((filter) =>
const lightsFilters = HOME_SUMMARIES_FILTERS.lights.map((filter) =>
generateEntityFilter(hass, filter)
);
@@ -144,6 +141,6 @@ export class LightsViewStrategy extends ReactiveElement {
declare global {
interface HTMLElementTagNameMap {
"lights-view-strategy": LightsViewStrategy;
"home-lights-view-strategy": HomeLightsViewStrategy;
}
}
@@ -131,7 +131,7 @@ export class HomeMainViewStrategy extends ReactiveElement {
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/lights?historyBack=1",
navigation_path: "lights",
},
grid_options: {
rows: 2,
@@ -144,7 +144,7 @@ export class HomeMainViewStrategy extends ReactiveElement {
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/climate?historyBack=1",
navigation_path: "climate",
},
grid_options: {
rows: 2,
@@ -157,7 +157,7 @@ export class HomeMainViewStrategy extends ReactiveElement {
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/security?historyBack=1",
navigation_path: "security",
},
grid_options: {
rows: 2,
@@ -1,9 +1,6 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import {
findEntities,
generateEntityFilter,
} from "../../../../common/entity/entity_filter";
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
@@ -11,7 +8,7 @@ import type { HomeAssistant } from "../../../../types";
import type { MediaControlCardConfig } from "../../cards/types";
import { getAreas, getFloors } from "../areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "./helpers/home-structure";
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
import { findEntities, HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
export interface HomeMediaPlayersViewStrategyConfig {
type: "home-media-players";
@@ -1,9 +1,6 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import {
findEntities,
generateEntityFilter,
} from "../../../../common/entity/entity_filter";
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
@@ -14,7 +11,7 @@ import {
getFloors,
} from "../areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "./helpers/home-structure";
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
import { findEntities, HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
export interface HomeSecurityViewStrategyConfig {
type: "home-security";
-3
View File
@@ -323,9 +323,6 @@ export const getMyRedirects = (): Redirects => ({
category: "string?",
},
},
lights: {
redirect: "/lights",
},
});
const getRedirect = (path: string): Redirect | undefined =>
+1 -1
View File
@@ -39,7 +39,7 @@ class HaSetVibrateRow extends LitElement {
fireEvent(this, "hass-vibrate", {
vibrate,
});
forwardHaptic("light");
forwardHaptic(this, "light");
}
}
-200
View File
@@ -1,200 +0,0 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { goBack } from "../../common/navigate";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-menu-button";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
const SECURITY_LOVELACE_CONFIG: LovelaceConfig = {
views: [
{
strategy: {
type: "security",
},
},
],
};
@customElement("ha-panel-security")
class PanelSecurity extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@state() private _viewIndex = 0;
@state() private _lovelace?: Lovelace;
@state() private _searchParms = new URLSearchParams(window.location.search);
public willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
}
if (!changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as this["hass"];
if (oldHass?.locale !== this.hass.locale) {
this._setLovelace();
}
}
private _back(ev) {
ev.stopPropagation();
goBack();
}
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.narrow
? html`<div class="main-title">
${this.hass.localize("panel.security")}
</div>`
: nothing}
</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 _setLovelace() {
this._lovelace = {
config: SECURITY_LOVELACE_CONFIG,
rawConfig: SECURITY_LOVELACE_CONFIG,
editMode: false,
urlPath: "security",
mode: "generated",
locale: this.hass.locale,
enableFullEditMode: () => undefined,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
showToast: () => undefined,
};
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
.header {
background-color: var(--app-header-background-color);
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
position: fixed;
top: 0;
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-right,
0px
)
);
padding-top: var(--safe-area-inset-top);
z-index: 4;
transition: box-shadow 200ms linear;
display: flex;
flex-direction: row;
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
backdrop-filter: var(--app-header-backdrop-filter, none);
padding-top: var(--safe-area-inset-top);
padding-right: var(--safe-area-inset-right);
}
:host([narrow]) .header {
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-left,
0px
) - var(--safe-area-inset-right, 0px)
);
padding-left: var(--safe-area-inset-left);
}
:host([scrolled]) .header {
box-shadow: var(
--mdc-top-app-bar-fixed-box-shadow,
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
);
}
.toolbar {
height: var(--header-height);
display: flex;
flex: 1;
align-items: center;
font-size: var(--ha-font-size-xl);
padding: 0px 12px;
font-weight: var(--ha-font-weight-normal);
box-sizing: border-box;
}
:host([narrow]) .toolbar {
padding: 0 4px;
}
.main-title {
margin: var(--margin-title);
line-height: var(--ha-line-height-normal);
flex-grow: 1;
}
hui-view-container {
position: relative;
display: flex;
min-height: 100vh;
box-sizing: border-box;
padding-top: calc(var(--header-height) + var(--safe-area-inset-top));
padding-right: var(--safe-area-inset-right);
padding-inline-end: var(--safe-area-inset-right);
padding-bottom: var(--safe-area-inset-bottom);
}
:host([narrow]) hui-view-container {
padding-left: var(--safe-area-inset-left);
padding-inline-start: var(--safe-area-inset-left);
}
hui-view {
flex: 1 1 100%;
max-width: 100%;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-panel-security": PanelSecurity;
}
}
@@ -1,191 +0,0 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import {
findEntities,
generateEntityFilter,
type EntityFilter,
} from "../../../common/entity/entity_filter";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import {
computeAreaTileCardConfig,
getAreas,
getFloors,
} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure";
export interface SecurityViewStrategyConfig {
type: "security";
}
export const securityEntityFilters: EntityFilter[] = [
{
domain: "camera",
entity_category: "none",
},
{
domain: "alarm_control_panel",
entity_category: "none",
},
{
domain: "lock",
entity_category: "none",
},
{
domain: "cover",
device_class: ["door", "garage", "gate"],
entity_category: "none",
},
{
domain: "binary_sensor",
device_class: [
// Locks
"lock",
// Openings
"door",
"window",
"garage_door",
"opening",
// Safety
"carbon_monoxide",
"gas",
"moisture",
"safety",
"smoke",
"tamper",
],
entity_category: "none",
},
// We also want the tamper sensors when they are diagnostic
{
domain: "binary_sensor",
device_class: ["tamper"],
entity_category: "diagnostic",
},
];
const processAreasForSecurity = (
areaIds: string[],
hass: HomeAssistant,
entities: string[]
): LovelaceCardConfig[] => {
const cards: LovelaceCardConfig[] = [];
for (const areaId of areaIds) {
const area = hass.areas[areaId];
if (!area) continue;
const areaFilter = generateEntityFilter(hass, {
area: area.area_id,
});
const areaSecurityEntities = entities.filter(areaFilter);
const areaCards: LovelaceCardConfig[] = [];
const computeTileCard = computeAreaTileCardConfig(hass, "", false);
for (const entityId of areaSecurityEntities) {
areaCards.push(computeTileCard(entityId));
}
if (areaCards.length > 0) {
cards.push({
heading_style: "subtitle",
type: "heading",
heading: area.name,
});
cards.push(...areaCards);
}
}
return cards;
};
@customElement("security-view-strategy")
export class SecurityViewStrategy extends ReactiveElement {
static async generate(
_config: SecurityViewStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const areas = getAreas(hass.areas);
const floors = getFloors(hass.floors);
const home = getHomeStructure(floors, areas);
const sections: LovelaceSectionRawConfig[] = [];
const allEntities = Object.keys(hass.states);
const securityFilters = securityEntityFilters.map((filter) =>
generateEntityFilter(hass, filter)
);
const entities = findEntities(allEntities, securityFilters);
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
// Process floors
for (const floorStructure of home.floors) {
const floorId = floorStructure.id;
const areaIds = floorStructure.areas;
const floor = hass.floors[floorId];
const section: LovelaceSectionRawConfig = {
type: "grid",
column_span: 2,
cards: [
{
type: "heading",
heading:
floorCount > 1
? floor.name
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
},
],
};
const areaCards = processAreasForSecurity(areaIds, hass, entities);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);
sections.push(section);
}
}
// Process unassigned areas
if (home.areas.length > 0) {
const section: LovelaceSectionRawConfig = {
type: "grid",
column_span: 2,
cards: [
{
type: "heading",
heading:
floorCount > 1
? hass.localize("ui.panel.lovelace.strategy.home.other_areas")
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
},
],
};
const areaCards = processAreasForSecurity(home.areas, hass, entities);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);
sections.push(section);
}
}
return {
type: "sections",
max_columns: 2,
sections: sections || [],
};
}
}
declare global {
interface HTMLElementTagNameMap {
"security-view-strategy": SecurityViewStrategy;
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ export const coreStyles = css`
--ha-space-10: 40px;
--ha-space-11: 44px;
--ha-space-12: 48px;
--ha-space-13: 44px;
--ha-space-13: 52px;
--ha-space-14: 56px;
--ha-space-15: 60px;
--ha-space-16: 64px;
@@ -40,7 +40,7 @@ export class HaStateControlCoverToggle extends LitElement {
if (!this.hass || !this.stateObj) {
return;
}
forwardHaptic("light");
forwardHaptic(this, "light");
await this.hass.callService(
"cover",
+1 -1
View File
@@ -46,7 +46,7 @@ export class HaStateControlToggle extends LitElement {
if (!this.hass || !this.stateObj) {
return;
}
forwardHaptic("light");
forwardHaptic(this, "light");
const stateDomain = computeDomain(this.stateObj.entity_id);
let serviceDomain;
let service;
@@ -68,7 +68,7 @@ export class HaStateControlLockToggle extends LitElement {
if (!this.hass || !this.stateObj) {
return;
}
forwardHaptic("light");
forwardHaptic(this, "light");
fireEvent(this, "lock-service-called");
callProtectedLockService(
this,
@@ -40,7 +40,7 @@ export class HaStateControlValveToggle extends LitElement {
if (!this.hass || !this.stateObj) {
return;
}
forwardHaptic("light");
forwardHaptic(this, "light");
await this.hass.callService(
"valve",
+1 -1
View File
@@ -127,7 +127,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
);
}
if (notifyOnError) {
forwardHaptic("failure");
forwardHaptic(this, "failure");
const lokalize = await this.hass!.loadBackendTranslation(
"exceptions",
err.translation_domain
+26 -5
View File
@@ -10,10 +10,7 @@
"todo": "To-do lists",
"developer_tools": "Developer tools",
"media_browser": "Media",
"profile": "Profile",
"lights": "Lights",
"security": "Security",
"climate": "Climate"
"profile": "Profile"
},
"state": {
"default": {
@@ -667,20 +664,43 @@
},
"target-picker": {
"expand": "Expand",
"collapse": "Collapse",
"expand_floor_id": "Split this floor into separate areas.",
"expand_area_id": "Split this area into separate devices and entities.",
"expand_device_id": "Split this device into separate entities.",
"expand_label_id": "Split this label into separate areas, devices and entities.",
"remove": "Remove",
"remove_floor_id": "Remove floor",
"remove_floors": "Remove floors",
"remove_area_id": "Remove area",
"remove_areas": "Remove areas",
"remove_device_id": "Remove device",
"remove_devices": "Remove devices",
"remove_entity_id": "Remove entity",
"remove_entitys": "Remove entities",
"remove_label_id": "Remove label",
"remove_labels": "Remove labels",
"add_area_id": "Choose area",
"add_device_id": "Choose device",
"add_entity_id": "Choose entity",
"add_label_id": "Choose label"
"add_label_id": "Choose label",
"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",
"selected": {
"entity": "Entities: {count}",
"device": "Devices: {count}",
"area": "Areas: {count}",
"label": "Labels: {count}",
"floor": "Floors: {count}"
},
"type": {
"area": "Area",
"device": "Device",
"entity": "Entity",
"label": "Label",
"floor": "Floor"
}
},
"subpage-data-table": {
"filters": "Filters",
@@ -6887,6 +6907,7 @@
"home": {
"summary_list": {
"climate": "Climate",
"lights": "Lights",
"security": "Security",
"media_players": "Media players"
},
+10 -18
View File
@@ -1351,10 +1351,10 @@ __metadata:
languageName: node
linkType: hard
"@ctrl/tinycolor@npm:^4.1.0":
version: 4.2.0
resolution: "@ctrl/tinycolor@npm:4.2.0"
checksum: 10/1be14de7d7e8184c0bc5c8d7e3486cc8186e6702e8ca899c7239f328bb1df9a15d1575e2af7b4c6ba020727fa78f5a9f887555971f30a2890cece9e4253a9d3a
"@ctrl/tinycolor@npm:4.1.0":
version: 4.1.0
resolution: "@ctrl/tinycolor@npm:4.1.0"
checksum: 10/e64569399139ef0abd2eb0ec9fb7267dfd7820f7ad7d4567a63e5fc35e5cfdcb8ecdb3bad65cb9244b47ba6c77bc51085826c00e981acf263a3221dc89343adc
languageName: node
linkType: hard
@@ -1940,11 +1940,11 @@ __metadata:
languageName: node
linkType: hard
"@home-assistant/webawesome@npm:3.0.0-beta.4.ha.3":
version: 3.0.0-beta.4.ha.3
resolution: "@home-assistant/webawesome@npm:3.0.0-beta.4.ha.3"
"@home-assistant/webawesome@npm:3.0.0-beta.6.ha.0":
version: 3.0.0-beta.6.ha.0
resolution: "@home-assistant/webawesome@npm:3.0.0-beta.6.ha.0"
dependencies:
"@ctrl/tinycolor": "npm:^4.1.0"
"@ctrl/tinycolor": "npm:4.1.0"
"@floating-ui/dom": "npm:^1.6.13"
"@lit/react": "npm:^1.0.8"
"@shoelace-style/animations": "npm:^1.2.0"
@@ -1953,8 +1953,7 @@ __metadata:
lit: "npm:^3.2.1"
nanoid: "npm:^5.1.5"
qr-creator: "npm:^1.0.0"
style-observer: "npm:^0.0.7"
checksum: 10/b9241821ed471ccbad86b0ea4697a2d41395f05fdc26f46e5edbc7f6b5eeab5d248251ef702326312ded00d5bf850ce0dcdcf7cd5e2e542b9d9cb9a84f3726da
checksum: 10/ec9d74585b544e5755f7b2644a0d7f9318b5776bedf51430c8f8729918fddb6e54cce46acace674960383385362846cc4c0f2da5245fa622bce8c54733a31865
languageName: node
linkType: hard
@@ -9195,7 +9194,7 @@ __metadata:
"@fullcalendar/list": "npm:6.1.19"
"@fullcalendar/luxon3": "npm:6.1.19"
"@fullcalendar/timegrid": "npm:6.1.19"
"@home-assistant/webawesome": "npm:3.0.0-beta.4.ha.3"
"@home-assistant/webawesome": "npm:3.0.0-beta.6.ha.0"
"@lezer/highlight": "npm:1.2.1"
"@lit-labs/motion": "npm:1.0.9"
"@lit-labs/observers": "npm:2.0.6"
@@ -13725,13 +13724,6 @@ __metadata:
languageName: node
linkType: hard
"style-observer@npm:^0.0.7":
version: 0.0.7
resolution: "style-observer@npm:0.0.7"
checksum: 10/bb57f98bae4463c1e1b57234f8ffe72ec0de27fb08b032c1919910129c210aacd1ddd615432b9453d491e10d3b719cf6c2a68a97165ca55d6fc9b86c0fca37fb
languageName: node
linkType: hard
"style-observer@npm:^0.0.8":
version: 0.0.8
resolution: "style-observer@npm:0.0.8"