Compare commits

...

16 Commits

Author SHA1 Message Date
Paul Bottein
134681b4c9 Merge branch 'dev' into toggle_group_dialog 2025-07-10 18:50:14 +02:00
Paul Bottein
082f1ca55e Center content on mobile 2025-07-10 18:49:05 +02:00
Paul Bottein
341e63e878 Fix device class icon off state 2025-07-10 17:36:03 +02:00
Paul Bottein
5ed2d2fd2f Fix last updated 2025-07-10 17:33:15 +02:00
Paul Bottein
c6f92d1375 Add translations 2025-07-10 17:26:36 +02:00
Paul Bottein
e8201f7848 Use variable 2025-07-10 15:59:53 +02:00
Paul Bottein
6d7df18e82 Fix available entities and header 2025-07-10 15:58:09 +02:00
Paul Bottein
1471cfea66 Don't use new colors for now 2025-07-10 15:33:29 +02:00
Paul Bottein
9e4835107d Merge dialog with more info 2025-07-10 14:43:46 +02:00
Paul Bottein
3269fd3c5b Feedbacks 2025-07-09 18:14:29 +02:00
Paul Bottein
17e63343c7 Handle multiple entities 2025-07-09 16:52:58 +02:00
Paul Bottein
dc7ba0dac6 Fix dialog at the top 2025-07-09 10:02:12 +02:00
Paul Bottein
2ab4608884 Delete dashboard dialog 2025-07-09 09:49:44 +02:00
Paul Bottein
de7f5c1bb7 Add toggle group dialog 2025-07-08 19:18:13 +02:00
Paul Bottein
7144b7802e Invert order 2025-07-08 15:06:59 +02:00
Paul Bottein
ca315b88ce Add sections dialog 2025-07-08 12:35:51 +02:00
10 changed files with 539 additions and 218 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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