mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-03 07:59:43 +00:00
Compare commits
16 Commits
20250903.3
...
toggle_gro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
134681b4c9 | ||
|
|
082f1ca55e | ||
|
|
341e63e878 | ||
|
|
5ed2d2fd2f | ||
|
|
c6f92d1375 | ||
|
|
e8201f7848 | ||
|
|
6d7df18e82 | ||
|
|
1471cfea66 | ||
|
|
9e4835107d | ||
|
|
3269fd3c5b | ||
|
|
17e63343c7 | ||
|
|
dc7ba0dac6 | ||
|
|
2ab4608884 | ||
|
|
de7f5c1bb7 | ||
|
|
7144b7802e | ||
|
|
ca315b88ce |
@@ -31,7 +31,8 @@ export type LocalizeKeys =
|
||||
| `ui.panel.lovelace.card.${string}`
|
||||
| `ui.panel.lovelace.editor.${string}`
|
||||
| `ui.panel.page-authorize.form.${string}`
|
||||
| `component.${string}`;
|
||||
| `component.${string}`
|
||||
| `ui.entity.${string}`;
|
||||
|
||||
export type LandingPageKeys = FlattenObjectKeys<
|
||||
TranslationDict["landing-page"]
|
||||
|
||||
@@ -72,6 +72,9 @@ export class HaControlButton extends LitElement {
|
||||
color 180ms ease-in-out;
|
||||
color: var(--control-button-icon-color);
|
||||
}
|
||||
:host([vertical]) .button {
|
||||
flex-direction: column;
|
||||
}
|
||||
.button:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--control-button-focus-color);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../../../common/entity/compute_state_domain";
|
||||
import { computeGroupEntitiesState } from "../../../../common/entity/group_entities";
|
||||
import "../../../../components/ha-control-button";
|
||||
import "../../../../components/ha-control-button-group";
|
||||
import "../../../../components/ha-domain-icon";
|
||||
import { isFullyClosed, isFullyOpen } from "../../../../data/cover";
|
||||
import { OFF, ON, UNAVAILABLE } from "../../../../data/entity";
|
||||
import { forwardHaptic } from "../../../../data/haptics";
|
||||
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import type { TileCardConfig } from "../../../../panels/lovelace/cards/types";
|
||||
import "../../../../panels/lovelace/sections/hui-section";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../ha-more-info-state-header";
|
||||
|
||||
export interface GroupToggleDialogParams {
|
||||
entityIds: string[];
|
||||
}
|
||||
|
||||
@customElement("ha-more-info-view-toggle-group")
|
||||
class HaMoreInfoViewToggleGroup extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public params?: GroupToggleDialogParams;
|
||||
|
||||
private _sectionConfig = memoizeOne(
|
||||
(entities: string[]): LovelaceSectionConfig => ({
|
||||
type: "grid",
|
||||
cards: entities.map<TileCardConfig>((entity) => ({
|
||||
type: "tile",
|
||||
entity: entity,
|
||||
icon_tap_action: {
|
||||
action: "toggle",
|
||||
},
|
||||
tap_action: {
|
||||
action: "more-info",
|
||||
},
|
||||
grid_options: {
|
||||
columns: 12,
|
||||
},
|
||||
})),
|
||||
})
|
||||
);
|
||||
|
||||
private _combineEntities(entities: HassEntity[]): HassEntity {
|
||||
const firstEntity = entities[0];
|
||||
const domain = computeStateDomain(firstEntity);
|
||||
|
||||
const combined: HassEntity = {
|
||||
entity_id: `${domain}.all_entities`,
|
||||
state: computeGroupEntitiesState(entities),
|
||||
attributes: {
|
||||
device_class: firstEntity.attributes.device_class,
|
||||
},
|
||||
last_changed: new Date(
|
||||
Math.max(...entities.map((e) => new Date(e.last_changed).getTime()))
|
||||
).toISOString(),
|
||||
last_updated: new Date(
|
||||
Math.max(...entities.map((e) => new Date(e.last_updated).getTime()))
|
||||
).toISOString(),
|
||||
context: {
|
||||
id: "",
|
||||
parent_id: "",
|
||||
user_id: "",
|
||||
},
|
||||
};
|
||||
return combined;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const sectionConfig = this._sectionConfig(this.params.entityIds);
|
||||
|
||||
const entities = this.params.entityIds
|
||||
.map((entityId) => this.hass!.states[entityId] as HassEntity | undefined)
|
||||
.filter((v): v is HassEntity => Boolean(v));
|
||||
|
||||
const groupStateObj = this._combineEntities(entities);
|
||||
|
||||
const formattedGroupState = this.hass.formatEntityState(groupStateObj);
|
||||
|
||||
const domain = computeStateDomain(groupStateObj);
|
||||
|
||||
const deviceClass = groupStateObj.attributes.device_class;
|
||||
|
||||
const availableEntities = entities.filter(
|
||||
(entity) => entity.state !== UNAVAILABLE
|
||||
);
|
||||
|
||||
const ON_STATE = domain === "cover" ? "open" : ON;
|
||||
const OFF_STATE = domain === "cover" ? "closed" : OFF;
|
||||
|
||||
const isAllOn = availableEntities.every((entity) =>
|
||||
computeDomain(entity.entity_id) === "cover"
|
||||
? isFullyOpen(entity)
|
||||
: entity.state === ON_STATE
|
||||
);
|
||||
const isAllOff = availableEntities.every((entity) =>
|
||||
computeDomain(entity.entity_id) === "cover"
|
||||
? isFullyClosed(entity)
|
||||
: entity.state === OFF_STATE
|
||||
);
|
||||
|
||||
const isMultiple = this.params.entityIds.length > 1;
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<ha-more-info-state-header
|
||||
.hass=${this.hass}
|
||||
.stateObj=${groupStateObj}
|
||||
.stateOverride=${formattedGroupState}
|
||||
></ha-more-info-state-header>
|
||||
<div class="main">
|
||||
<ha-control-button-group vertical>
|
||||
<ha-control-button
|
||||
vertical
|
||||
@click=${this._turnAllOn}
|
||||
.disabled=${isAllOn}
|
||||
>
|
||||
<ha-domain-icon
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
.state=${ON_STATE}
|
||||
.deviceClass=${deviceClass}
|
||||
></ha-domain-icon>
|
||||
<p>
|
||||
${domain === "cover"
|
||||
? isMultiple
|
||||
? this.hass.localize("ui.card.cover.open_all")
|
||||
: this.hass.localize("ui.card.cover.open")
|
||||
: isMultiple
|
||||
? this.hass.localize("ui.card.common.turn_on_all")
|
||||
: this.hass.localize("ui.card.common.turn_on")}
|
||||
</p>
|
||||
</ha-control-button>
|
||||
<ha-control-button
|
||||
vertical
|
||||
@click=${this._turnAllOff}
|
||||
.disabled=${isAllOff}
|
||||
>
|
||||
<ha-domain-icon
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
.state=${OFF_STATE}
|
||||
.deviceClass=${deviceClass}
|
||||
.icon=${domain === "light" ? "mdi:lightbulb-off" : undefined}
|
||||
></ha-domain-icon>
|
||||
|
||||
<p>
|
||||
${domain === "cover"
|
||||
? isMultiple
|
||||
? this.hass.localize("ui.card.cover.close_all")
|
||||
: this.hass.localize("ui.card.cover.close")
|
||||
: isMultiple
|
||||
? this.hass.localize("ui.card.common.turn_off_all")
|
||||
: this.hass.localize("ui.card.common.turn_off")}
|
||||
</p>
|
||||
</ha-control-button>
|
||||
</ha-control-button-group>
|
||||
</div>
|
||||
|
||||
<div class="entities">
|
||||
<hui-section
|
||||
.config=${sectionConfig}
|
||||
.hass=${this.hass}
|
||||
></hui-section>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _turnAllOff() {
|
||||
if (!this.params) {
|
||||
return;
|
||||
}
|
||||
|
||||
forwardHaptic("light");
|
||||
const domain = computeDomain(this.params.entityIds[0]);
|
||||
if (domain === "cover") {
|
||||
this.hass.callService("cover", "close_cover", {
|
||||
entity_id: this.params.entityIds,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.hass.callService("homeassistant", "turn_off", {
|
||||
entity_id: this.params.entityIds,
|
||||
});
|
||||
}
|
||||
|
||||
private _turnAllOn() {
|
||||
if (!this.params) {
|
||||
return;
|
||||
}
|
||||
|
||||
forwardHaptic("light");
|
||||
const domain = computeDomain(this.params.entityIds[0]);
|
||||
if (domain === "cover") {
|
||||
this.hass.callService("cover", "open_cover", {
|
||||
entity_id: this.params.entityIds,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.hass.callService("homeassistant", "turn_on", {
|
||||
entity_id: this.params.entityIds,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
ha-more-info-state-header {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ha-control-button-group {
|
||||
--control-button-group-spacing: 12px;
|
||||
--control-button-group-thickness: 130px;
|
||||
}
|
||||
ha-control-button {
|
||||
--control-button-border-radius: 16px;
|
||||
--mdc-icon-size: 24px;
|
||||
--control-button-padding: 16px 8px;
|
||||
--control-button-background-opacity: 0.1;
|
||||
}
|
||||
ha-control-button p {
|
||||
margin: 0;
|
||||
}
|
||||
.entities {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 12px;
|
||||
padding-bottom: max(var(--safe-area-inset-bottom), 12px);
|
||||
}
|
||||
hui-section {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-view-toggle-group": HaMoreInfoViewToggleGroup;
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,9 @@ export const showVoiceAssistantsView = (
|
||||
title: string
|
||||
): void => {
|
||||
fireEvent(element, "show-child-view", {
|
||||
viewTag: "ha-more-info-view-voice-assistants",
|
||||
viewImport: loadVoiceAssistantsView,
|
||||
viewTitle: title,
|
||||
viewParams: {},
|
||||
tag: "ha-more-info-view-voice-assistants",
|
||||
import: loadVoiceAssistantsView,
|
||||
title: title,
|
||||
params: {},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
mdiPencilOutline,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
@@ -68,15 +68,24 @@ export interface MoreInfoDialogParams {
|
||||
view?: View;
|
||||
/** @deprecated Use `view` instead */
|
||||
tab?: View;
|
||||
parentView?: ParentView;
|
||||
}
|
||||
|
||||
type View = "info" | "history" | "settings" | "related";
|
||||
type View = "info" | "history" | "settings" | "related" | "parent";
|
||||
|
||||
interface ParentView {
|
||||
tag: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
import?: () => Promise<unknown>;
|
||||
params?: any;
|
||||
}
|
||||
|
||||
interface ChildView {
|
||||
viewTag: string;
|
||||
viewTitle?: string;
|
||||
viewImport?: () => Promise<unknown>;
|
||||
viewParams?: any;
|
||||
tag: string;
|
||||
title?: string;
|
||||
import?: () => Promise<unknown>;
|
||||
params?: any;
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -88,7 +97,8 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_VIEW: View = "info";
|
||||
const INFO_VIEW: View = "info";
|
||||
const PARENT_VIEW: View = "parent";
|
||||
|
||||
@customElement("ha-more-info-dialog")
|
||||
export class MoreInfoDialog extends LitElement {
|
||||
@@ -98,9 +108,11 @@ export class MoreInfoDialog extends LitElement {
|
||||
|
||||
@state() private _entityId?: string | null;
|
||||
|
||||
@state() private _currView: View = DEFAULT_VIEW;
|
||||
@state() private _currView: View = INFO_VIEW;
|
||||
|
||||
@state() private _initialView: View = DEFAULT_VIEW;
|
||||
@state() private _initialView: View = INFO_VIEW;
|
||||
|
||||
@state() private _parentView?: ParentView;
|
||||
|
||||
@state() private _childView?: ChildView;
|
||||
|
||||
@@ -114,17 +126,29 @@ export class MoreInfoDialog extends LitElement {
|
||||
|
||||
public showDialog(params: MoreInfoDialogParams) {
|
||||
this._entityId = params.entityId;
|
||||
if (!this._entityId) {
|
||||
if (!this._entityId && !params.parentView) {
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
this._currView = params.view || DEFAULT_VIEW;
|
||||
this._initialView = params.view || DEFAULT_VIEW;
|
||||
this._parentView = params.parentView;
|
||||
if (this._parentView?.import) {
|
||||
this._parentView.import();
|
||||
this._currView = PARENT_VIEW;
|
||||
} else {
|
||||
this._currView = params.view || INFO_VIEW;
|
||||
}
|
||||
this._initialView = params.view || INFO_VIEW;
|
||||
this._childView = undefined;
|
||||
this.large = false;
|
||||
this._loadEntityRegistryEntry();
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has("_entityId")) {
|
||||
this._loadEntityRegistryEntry();
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadEntityRegistryEntry() {
|
||||
if (!this._entityId) {
|
||||
return;
|
||||
@@ -143,19 +167,18 @@ export class MoreInfoDialog extends LitElement {
|
||||
this._entityId = undefined;
|
||||
this._entry = undefined;
|
||||
this._childView = undefined;
|
||||
this._parentView = undefined;
|
||||
this._currView = INFO_VIEW;
|
||||
this._infoEditMode = false;
|
||||
this._initialView = DEFAULT_VIEW;
|
||||
this._initialView = INFO_VIEW;
|
||||
this._isEscapeEnabled = true;
|
||||
window.removeEventListener("dialog-closed", this._enableEscapeKeyClose);
|
||||
window.removeEventListener("show-dialog", this._disableEscapeKeyClose);
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private _shouldShowEditIcon(
|
||||
domain: string,
|
||||
stateObj: HassEntity | undefined
|
||||
): boolean {
|
||||
if (__DEMO__ || !stateObj) {
|
||||
private _shouldShowEditIcon(domain?: string, stateObj?: HassEntity): boolean {
|
||||
if (__DEMO__ || !stateObj || !domain) {
|
||||
return false;
|
||||
}
|
||||
if (EDITABLE_DOMAINS_WITH_ID.includes(domain) && stateObj.attributes.id) {
|
||||
@@ -171,8 +194,9 @@ export class MoreInfoDialog extends LitElement {
|
||||
return false;
|
||||
}
|
||||
|
||||
private _shouldShowHistory(domain: string): boolean {
|
||||
private _shouldShowHistory(domain?: string): boolean {
|
||||
return (
|
||||
domain !== undefined &&
|
||||
DOMAINS_WITH_MORE_INFO.includes(domain) &&
|
||||
(computeShowHistoryComponent(this.hass, this._entityId!) ||
|
||||
computeShowLogBookComponent(
|
||||
@@ -207,14 +231,30 @@ export class MoreInfoDialog extends LitElement {
|
||||
private _goBack() {
|
||||
if (this._childView) {
|
||||
this._childView = undefined;
|
||||
} else {
|
||||
this._setView(this._initialView);
|
||||
return;
|
||||
}
|
||||
const previousView = this._previousView();
|
||||
if (previousView) {
|
||||
this._setView(previousView);
|
||||
}
|
||||
}
|
||||
|
||||
private _previousView(): View | undefined {
|
||||
if (this._currView === PARENT_VIEW) {
|
||||
return undefined;
|
||||
}
|
||||
if (this._currView !== this._initialView) {
|
||||
return this._initialView;
|
||||
}
|
||||
if (this._parentView) {
|
||||
return PARENT_VIEW;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _resetInitialView() {
|
||||
this._initialView = DEFAULT_VIEW;
|
||||
this._setView(DEFAULT_VIEW);
|
||||
this._initialView = INFO_VIEW;
|
||||
this._setView(INFO_VIEW);
|
||||
}
|
||||
|
||||
private _goToHistory() {
|
||||
@@ -227,8 +267,8 @@ export class MoreInfoDialog extends LitElement {
|
||||
|
||||
private _showChildView(ev: CustomEvent): void {
|
||||
const view = ev.detail as ChildView;
|
||||
if (view.viewImport) {
|
||||
view.viewImport();
|
||||
if (view.import) {
|
||||
view.import();
|
||||
}
|
||||
this._childView = view;
|
||||
}
|
||||
@@ -286,45 +326,99 @@ export class MoreInfoDialog extends LitElement {
|
||||
this._sensorNumericDeviceClasses = deviceClasses.numeric_device_classes;
|
||||
}
|
||||
|
||||
private _handleMoreInfoEvent(ev: CustomEvent) {
|
||||
// If the parent view has a `show-dialog` event to open more info, we handle it here to set the entity ID and view.
|
||||
const detail = ev.detail as MoreInfoDialogParams;
|
||||
if (detail.entityId) {
|
||||
this._entityId = detail.entityId;
|
||||
this._setView(detail.view || INFO_VIEW);
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private _renderHeader = (): TemplateResult | typeof nothing => {
|
||||
if (this._parentView && this._currView === PARENT_VIEW) {
|
||||
return html`
|
||||
${this._parentView
|
||||
? html`<p class="breadcrumb">${this._parentView.subtitle}</p>`
|
||||
: nothing}
|
||||
<p class="main">${this._parentView.title}</p>
|
||||
`;
|
||||
}
|
||||
|
||||
const entityId = this._entityId;
|
||||
|
||||
if (entityId) {
|
||||
const stateObj = this.hass.states[entityId] as HassEntity | undefined;
|
||||
const context = stateObj
|
||||
? getEntityContext(stateObj, this.hass)
|
||||
: this._entry
|
||||
? getEntityEntryContext(this._entry, this.hass)
|
||||
: undefined;
|
||||
|
||||
const entityName = stateObj
|
||||
? computeEntityName(stateObj, this.hass)
|
||||
: this._entry
|
||||
? computeEntityEntryName(this._entry, this.hass)
|
||||
: entityId;
|
||||
|
||||
const deviceName = context?.device
|
||||
? computeDeviceName(context.device)
|
||||
: undefined;
|
||||
const areaName = context?.area
|
||||
? computeAreaName(context.area)
|
||||
: undefined;
|
||||
|
||||
const breadcrumb = [areaName, deviceName, entityName].filter(
|
||||
(v): v is string => Boolean(v)
|
||||
);
|
||||
const title = this._childView?.title || breadcrumb.pop() || entityId;
|
||||
const isAdmin = this.hass.user!.is_admin;
|
||||
|
||||
return html`
|
||||
${breadcrumb.length > 0
|
||||
? !__DEMO__ && isAdmin
|
||||
? html`
|
||||
<button
|
||||
class="breadcrumb"
|
||||
@click=${this._breadcrumbClick}
|
||||
aria-label=${breadcrumb.join(" > ")}
|
||||
>
|
||||
${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)}
|
||||
</button>
|
||||
`
|
||||
: html`
|
||||
<p class="breadcrumb">
|
||||
${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)}
|
||||
</p>
|
||||
`
|
||||
: nothing}
|
||||
<p class="main">${title}</p>
|
||||
`;
|
||||
}
|
||||
|
||||
return nothing;
|
||||
};
|
||||
|
||||
protected render() {
|
||||
if (!this._entityId) {
|
||||
if (!this._entityId && !this._parentView) {
|
||||
return nothing;
|
||||
}
|
||||
const entityId = this._entityId;
|
||||
const stateObj = this.hass.states[entityId] as HassEntity | undefined;
|
||||
const stateObj = entityId ? this.hass.states[entityId] : undefined;
|
||||
|
||||
const domain = computeDomain(entityId);
|
||||
const domain = entityId ? computeDomain(entityId) : undefined;
|
||||
|
||||
const isAdmin = this.hass.user!.is_admin;
|
||||
|
||||
const deviceId = this._getDeviceId();
|
||||
|
||||
const isDefaultView = this._currView === DEFAULT_VIEW && !this._childView;
|
||||
const previousView = this._previousView();
|
||||
const isDefaultView = this._currView === INFO_VIEW && !this._childView;
|
||||
const isSpecificInitialView =
|
||||
this._initialView !== DEFAULT_VIEW && !this._childView;
|
||||
const showCloseIcon = isDefaultView || isSpecificInitialView;
|
||||
|
||||
const context = stateObj
|
||||
? getEntityContext(stateObj, this.hass)
|
||||
: this._entry
|
||||
? getEntityEntryContext(this._entry, this.hass)
|
||||
: undefined;
|
||||
|
||||
const entityName = stateObj
|
||||
? computeEntityName(stateObj, this.hass)
|
||||
: this._entry
|
||||
? computeEntityEntryName(this._entry, this.hass)
|
||||
: entityId;
|
||||
|
||||
const deviceName = context?.device
|
||||
? computeDeviceName(context.device)
|
||||
: undefined;
|
||||
const areaName = context?.area ? computeAreaName(context.area) : undefined;
|
||||
|
||||
const breadcrumb = [areaName, deviceName, entityName].filter(
|
||||
(v): v is string => Boolean(v)
|
||||
);
|
||||
const title = this._childView?.viewTitle || breadcrumb.pop() || entityId;
|
||||
this._initialView !== INFO_VIEW && !this._childView;
|
||||
const showCloseIcon = !previousView && !this._childView;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
@@ -332,7 +426,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
@closed=${this.closeDialog}
|
||||
@opened=${this._handleOpened}
|
||||
.escapeKeyAction=${this._isEscapeEnabled ? undefined : ""}
|
||||
.heading=${title}
|
||||
.heading=${" "}
|
||||
hideActions
|
||||
flexContent
|
||||
>
|
||||
@@ -356,24 +450,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
></ha-icon-button-prev>
|
||||
`}
|
||||
<span slot="title" @click=${this._enlarge} class="title">
|
||||
${breadcrumb.length > 0
|
||||
? !__DEMO__ && isAdmin
|
||||
? html`
|
||||
<button
|
||||
class="breadcrumb"
|
||||
@click=${this._breadcrumbClick}
|
||||
aria-label=${breadcrumb.join(" > ")}
|
||||
>
|
||||
${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)}
|
||||
</button>
|
||||
`
|
||||
: html`
|
||||
<p class="breadcrumb">
|
||||
${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)}
|
||||
</p>
|
||||
`
|
||||
: nothing}
|
||||
<p class="main">${title}</p>
|
||||
${this._renderHeader()}
|
||||
</span>
|
||||
${isDefaultView
|
||||
? html`
|
||||
@@ -521,54 +598,62 @@ export class MoreInfoDialog extends LitElement {
|
||||
@show-child-view=${this._showChildView}
|
||||
@entity-entry-updated=${this._entryUpdated}
|
||||
@toggle-edit-mode=${this._handleToggleInfoEditModeEvent}
|
||||
@hass-more-info=${this._handleMoreInfoEvent}
|
||||
>
|
||||
${cache(
|
||||
this._childView
|
||||
? html`
|
||||
<div class="child-view">
|
||||
${dynamicElement(this._childView.viewTag, {
|
||||
${dynamicElement(this._childView.tag, {
|
||||
hass: this.hass,
|
||||
entry: this._entry,
|
||||
params: this._childView.viewParams,
|
||||
params: this._childView.params,
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: this._currView === "info"
|
||||
? html`
|
||||
<ha-more-info-info
|
||||
dialogInitialFocus
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
.entry=${this._entry}
|
||||
.editMode=${this._infoEditMode}
|
||||
></ha-more-info-info>
|
||||
`
|
||||
: this._currView === "history"
|
||||
: this._currView === "parent"
|
||||
? dynamicElement(this._parentView!.tag, {
|
||||
hass: this.hass,
|
||||
entry: this._entry,
|
||||
params: this._parentView!.params,
|
||||
})
|
||||
: this._currView === "info"
|
||||
? html`
|
||||
<ha-more-info-history-and-logbook
|
||||
<ha-more-info-info
|
||||
dialogInitialFocus
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
></ha-more-info-history-and-logbook>
|
||||
.entry=${this._entry}
|
||||
.editMode=${this._infoEditMode}
|
||||
></ha-more-info-info>
|
||||
`
|
||||
: this._currView === "settings"
|
||||
: this._currView === "history"
|
||||
? html`
|
||||
<ha-more-info-settings
|
||||
<ha-more-info-history-and-logbook
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
.entry=${this._entry}
|
||||
></ha-more-info-settings>
|
||||
></ha-more-info-history-and-logbook>
|
||||
`
|
||||
: this._currView === "related"
|
||||
: this._currView === "settings"
|
||||
? html`
|
||||
<ha-related-items
|
||||
<ha-more-info-settings
|
||||
.hass=${this.hass}
|
||||
.itemId=${entityId}
|
||||
.itemType=${SearchableDomains.has(domain)
|
||||
? (domain as ItemType)
|
||||
: "entity"}
|
||||
></ha-related-items>
|
||||
.entityId=${this._entityId}
|
||||
.entry=${this._entry}
|
||||
></ha-more-info-settings>
|
||||
`
|
||||
: nothing
|
||||
: this._currView === "related"
|
||||
? html`
|
||||
<ha-related-items
|
||||
.hass=${this.hass}
|
||||
.itemId=${entityId}
|
||||
.itemType=${domain &&
|
||||
SearchableDomains.has(domain)
|
||||
? (domain as ItemType)
|
||||
: "entity"}
|
||||
></ha-related-items>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
|
||||
@@ -5,11 +5,9 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../../common/array/ensure-array";
|
||||
import { computeAreaName } from "../../../common/entity/compute_area_name";
|
||||
import { generateEntityFilter } from "../../../common/entity/entity_filter";
|
||||
import {
|
||||
computeGroupEntitiesState,
|
||||
toggleGroupEntities,
|
||||
} from "../../../common/entity/group_entities";
|
||||
import { computeGroupEntitiesState } from "../../../common/entity/group_entities";
|
||||
import { stateActive } from "../../../common/entity/state_active";
|
||||
import { domainColorProperties } from "../../../common/entity/state_color";
|
||||
import "../../../components/ha-control-button";
|
||||
@@ -17,7 +15,8 @@ import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-domain-icon";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { forwardHaptic } from "../../../data/haptics";
|
||||
import type { GroupToggleDialogParams } from "../../../dialogs/more-info/components/voice/ha-more-info-view-toggle-group";
|
||||
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import { computeCssVariable } from "../../../resources/css-variables";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { AreaCardFeatureContext } from "../cards/hui-area-card";
|
||||
@@ -32,39 +31,24 @@ import type {
|
||||
import { AREA_CONTROLS } from "./types";
|
||||
|
||||
interface AreaControlsButton {
|
||||
offIcon?: string;
|
||||
onIcon?: string;
|
||||
filter: {
|
||||
domain: string;
|
||||
device_class?: string;
|
||||
};
|
||||
domain: string;
|
||||
device_class?: string;
|
||||
}
|
||||
|
||||
const coverButton = (deviceClass: string) => ({
|
||||
filter: {
|
||||
domain: "cover",
|
||||
device_class: deviceClass,
|
||||
},
|
||||
domain: "cover",
|
||||
device_class: deviceClass,
|
||||
});
|
||||
|
||||
export const AREA_CONTROLS_BUTTONS: Record<AreaControl, AreaControlsButton> = {
|
||||
light: {
|
||||
// Overrides the icons for lights
|
||||
offIcon: "mdi:lightbulb-off",
|
||||
onIcon: "mdi:lightbulb",
|
||||
filter: {
|
||||
domain: "light",
|
||||
},
|
||||
domain: "light",
|
||||
},
|
||||
fan: {
|
||||
filter: {
|
||||
domain: "fan",
|
||||
},
|
||||
domain: "fan",
|
||||
},
|
||||
switch: {
|
||||
filter: {
|
||||
domain: "switch",
|
||||
},
|
||||
domain: "switch",
|
||||
},
|
||||
"cover-blind": coverButton("blind"),
|
||||
"cover-curtain": coverButton("curtain"),
|
||||
@@ -98,7 +82,8 @@ export const getAreaControlEntities = (
|
||||
const filter = generateEntityFilter(hass, {
|
||||
area: areaId,
|
||||
entity_category: "none",
|
||||
...controlButton.filter,
|
||||
domain: controlButton.domain,
|
||||
device_class: controlButton.device_class,
|
||||
});
|
||||
|
||||
acc[control] = Object.keys(hass.entities).filter(
|
||||
@@ -160,9 +145,7 @@ class HuiAreaControlsCardFeature
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _handleButtonTap(ev: MouseEvent) {
|
||||
ev.stopPropagation();
|
||||
|
||||
private async _handleButtonTap(ev: MouseEvent) {
|
||||
if (!this.context?.area_id || !this.hass || !this._config) {
|
||||
return;
|
||||
}
|
||||
@@ -178,12 +161,27 @@ class HuiAreaControlsCardFeature
|
||||
);
|
||||
const entitiesIds = controlEntities[control];
|
||||
|
||||
const entities = entitiesIds
|
||||
.map((entityId) => this.hass!.states[entityId] as HassEntity | undefined)
|
||||
.filter((v): v is HassEntity => Boolean(v));
|
||||
const { domain, device_class: dc } = AREA_CONTROLS_BUTTONS[control];
|
||||
|
||||
forwardHaptic("light");
|
||||
toggleGroupEntities(this.hass, entities);
|
||||
const domainName = this.hass.localize(
|
||||
`component.${domain}.entity_component.${dc ?? "_"}.name`
|
||||
);
|
||||
|
||||
showMoreInfoDialog(this, {
|
||||
entityId: null,
|
||||
parentView: {
|
||||
title: computeAreaName(this._area!) || "",
|
||||
subtitle: domainName,
|
||||
tag: "ha-more-info-view-toggle-group",
|
||||
import: () =>
|
||||
import(
|
||||
"../../../dialogs/more-info/components/voice/ha-more-info-view-toggle-group"
|
||||
),
|
||||
params: {
|
||||
entityIds: entitiesIds,
|
||||
} as GroupToggleDialogParams,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _controlEntities = memoizeOne(
|
||||
@@ -254,15 +252,22 @@ class HuiAreaControlsCardFeature
|
||||
? stateActive(entities[0], groupState)
|
||||
: false;
|
||||
|
||||
const label = this.hass!.localize(
|
||||
`ui.card_features.area_controls.${control}.${active ? "off" : "on"}`
|
||||
const domain = button.domain;
|
||||
const dc = button.device_class;
|
||||
|
||||
const domainName = this.hass!.localize(
|
||||
`component.${domain}.entity_component.${dc ?? "_"}.name`
|
||||
);
|
||||
|
||||
const icon = active ? button.onIcon : button.offIcon;
|
||||
const label = `${domainName}: ${this.hass!.localize(
|
||||
`ui.card_features.area_controls.open_more_info`
|
||||
)}`;
|
||||
|
||||
const domain = button.filter.domain;
|
||||
const deviceClass = button.filter.device_class
|
||||
? ensureArray(button.filter.device_class)[0]
|
||||
const icon =
|
||||
domain === "light" && !active ? "mdi:lightbulb-off" : undefined;
|
||||
|
||||
const deviceClass = button.device_class
|
||||
? ensureArray(button.device_class)[0]
|
||||
: undefined;
|
||||
|
||||
const activeColor = computeCssVariable(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { clamp } from "../../../../common/number/clamp";
|
||||
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
@@ -148,7 +149,22 @@ export class AreaViewStrategy extends ReactiveElement {
|
||||
hass.localize("ui.panel.lovelace.strategy.areas.groups.security"),
|
||||
AREA_STRATEGY_GROUP_ICONS.security
|
||||
),
|
||||
...security.map(computeTileCard),
|
||||
...security.map((entityId) => {
|
||||
const domain = computeDomain(entityId);
|
||||
if (domain === "camera") {
|
||||
return {
|
||||
type: "picture-entity",
|
||||
entity: entityId,
|
||||
show_state: false,
|
||||
show_name: false,
|
||||
grid_options: {
|
||||
columns: 6,
|
||||
rows: 2,
|
||||
},
|
||||
};
|
||||
}
|
||||
return computeTileCard(entityId);
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../../../../common/entity/compute_state_name";
|
||||
import type { EntityFilterFunc } from "../../../../../common/entity/entity_filter";
|
||||
import { generateEntityFilter } from "../../../../../common/entity/entity_filter";
|
||||
@@ -10,7 +9,6 @@ import {
|
||||
import type { AreaRegistryEntry } from "../../../../../data/area_registry";
|
||||
import { areaCompare } from "../../../../../data/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../../../../data/floor_registry";
|
||||
import type { LovelaceCardConfig } from "../../../../../data/lovelace/config/card";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { supportsAlarmModesCardFeature } from "../../../card-features/hui-alarm-modes-card-feature";
|
||||
import { supportsCoverOpenCloseCardFeature } from "../../../card-features/hui-cover-open-close-card-feature";
|
||||
@@ -210,7 +208,7 @@ export const getAreaGroupedEntities = (
|
||||
|
||||
export const computeAreaTileCardConfig =
|
||||
(hass: HomeAssistant, prefix: string, includeFeature?: boolean) =>
|
||||
(entity: string): LovelaceCardConfig => {
|
||||
(entity: string): TileCardConfig => {
|
||||
const stateObj = hass.states[entity];
|
||||
|
||||
const context: LovelaceCardFeatureContext = {
|
||||
@@ -219,21 +217,6 @@ export const computeAreaTileCardConfig =
|
||||
|
||||
const additionalCardConfig: Partial<TileCardConfig> = {};
|
||||
|
||||
const domain = computeDomain(entity);
|
||||
|
||||
if (domain === "camera") {
|
||||
return {
|
||||
type: "picture-entity",
|
||||
entity: entity,
|
||||
show_state: false,
|
||||
show_name: false,
|
||||
grid_options: {
|
||||
columns: 6,
|
||||
rows: 2,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let feature: LovelaceCardFeatureConfig | undefined;
|
||||
if (includeFeature) {
|
||||
if (supportsLightBrightnessCardFeature(hass, context)) {
|
||||
|
||||
@@ -30,6 +30,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
{
|
||||
entityId: ev.detail.entityId,
|
||||
view: ev.detail.view || ev.detail.tab,
|
||||
parentView: ev.detail.parentView,
|
||||
},
|
||||
() => import("../dialogs/more-info/ha-more-info-dialog")
|
||||
);
|
||||
|
||||
@@ -79,6 +79,8 @@
|
||||
"common": {
|
||||
"turn_on": "Turn on",
|
||||
"turn_off": "Turn off",
|
||||
"turn_on_all": "Turn on all",
|
||||
"turn_off_all": "Turn off all",
|
||||
"toggle": "Toggle",
|
||||
"entity_not_found": "Entity not found"
|
||||
},
|
||||
@@ -145,7 +147,11 @@
|
||||
"close_cover": "Close cover",
|
||||
"open_tilt_cover": "Open cover tilt",
|
||||
"close_tilt_cover": "Close cover tilt",
|
||||
"stop_cover": "Stop cover"
|
||||
"stop_cover": "Stop cover",
|
||||
"open": "Open",
|
||||
"open_all": "Open all",
|
||||
"close": "Close",
|
||||
"close_all": "Close all"
|
||||
},
|
||||
"fan": {
|
||||
"preset_mode": "Preset mode",
|
||||
@@ -327,58 +333,7 @@
|
||||
},
|
||||
"card_features": {
|
||||
"area_controls": {
|
||||
"light": {
|
||||
"on": "Turn on area lights",
|
||||
"off": "Turn off area lights"
|
||||
},
|
||||
"fan": {
|
||||
"on": "Turn on area fans",
|
||||
"off": "Turn off area fans"
|
||||
},
|
||||
"switch": {
|
||||
"on": "Turn on area switches",
|
||||
"off": "Turn off area switches"
|
||||
},
|
||||
"cover-awning": {
|
||||
"on": "Open area awnings",
|
||||
"off": "Close area awnings"
|
||||
},
|
||||
"cover-blind": {
|
||||
"on": "Open area blinds",
|
||||
"off": "Close area blinds"
|
||||
},
|
||||
"cover-curtain": {
|
||||
"on": "Open area curtains",
|
||||
"off": "Close area curtains"
|
||||
},
|
||||
"cover-damper": {
|
||||
"on": "Open area dampers",
|
||||
"off": "Close area dampers"
|
||||
},
|
||||
"cover-door": {
|
||||
"on": "Open area doors",
|
||||
"off": "Close area doors"
|
||||
},
|
||||
"cover-garage": {
|
||||
"on": "Open garage door",
|
||||
"off": "Close garage door"
|
||||
},
|
||||
"cover-gate": {
|
||||
"on": "Open area gates",
|
||||
"off": "Close area gates"
|
||||
},
|
||||
"cover-shade": {
|
||||
"on": "Open area shades",
|
||||
"off": "Close area shades"
|
||||
},
|
||||
"cover-shutter": {
|
||||
"on": "Open area shutters",
|
||||
"off": "Close area shutters"
|
||||
},
|
||||
"cover-window": {
|
||||
"on": "Open area windows",
|
||||
"off": "Close area windows"
|
||||
}
|
||||
"open_more_info": "Open more info"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
Reference in New Issue
Block a user