20250701.0 (#26019)

This commit is contained in:
Paul Bottein 2025-07-01 15:04:35 +02:00 committed by GitHub
commit c3f0bba4a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 415 additions and 286 deletions

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20250627.0" version = "20250701.0"
license = "Apache-2.0" license = "Apache-2.0"
license-files = ["LICENSE*"] license-files = ["LICENSE*"]
description = "The Home Assistant frontend" description = "The Home Assistant frontend"

View File

@ -1,4 +1,4 @@
import { callService, type HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import { computeStateDomain } from "./compute_state_domain"; import { computeStateDomain } from "./compute_state_domain";
import { isUnavailableState, UNAVAILABLE } from "../../data/entity"; import { isUnavailableState, UNAVAILABLE } from "../../data/entity";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
@ -62,7 +62,7 @@ export const toggleGroupEntities = (
const entitiesIds = states.map((stateObj) => stateObj.entity_id); const entitiesIds = states.map((stateObj) => stateObj.entity_id);
callService(hass.connection, domain, service, { hass.callService(domain, service, {
entity_id: entitiesIds, entity_id: entitiesIds,
}); });
}; };

View File

@ -1,14 +1,15 @@
import { mdiTextureBox } from "@mdi/js"; import { mdiDrag, mdiTextureBox } from "@mdi/js";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { computeFloorName } from "../common/entity/compute_floor_name"; import { computeFloorName } from "../common/entity/compute_floor_name";
import { getAreaContext } from "../common/entity/context/get_area_context"; import { getAreaContext } from "../common/entity/context/get_area_context";
import { stringCompare } from "../common/string/compare";
import { areaCompare } from "../data/area_registry"; import { areaCompare } from "../data/area_registry";
import type { FloorRegistryEntry } from "../data/floor_registry"; import type { FloorRegistryEntry } from "../data/floor_registry";
import { getFloors } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-expansion-panel"; import "./ha-expansion-panel";
import "./ha-floor-icon"; import "./ha-floor-icon";
@ -17,9 +18,14 @@ import type { DisplayItem, DisplayValue } from "./ha-items-display-editor";
import "./ha-svg-icon"; import "./ha-svg-icon";
import "./ha-textfield"; import "./ha-textfield";
export interface AreasDisplayValue { export interface AreasFloorsDisplayValue {
hidden?: string[]; areas_display?: {
order?: string[]; hidden?: string[];
order?: string[];
};
floors_display?: {
order?: string[];
};
} }
const UNASSIGNED_FLOOR = "__unassigned__"; const UNASSIGNED_FLOOR = "__unassigned__";
@ -30,12 +36,10 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
@property() public label?: string; @property() public label?: string;
@property({ attribute: false }) public value?: AreasDisplayValue; @property({ attribute: false }) public value?: AreasFloorsDisplayValue;
@property() public helper?: string; @property() public helper?: string;
@property({ type: Boolean }) public expanded = false;
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false; @property({ type: Boolean }) public required = false;
@ -44,55 +48,78 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
public showNavigationButton = false; public showNavigationButton = false;
protected render(): TemplateResult { protected render(): TemplateResult {
const groupedItems = this._groupedItems(this.hass.areas, this.hass.floors); const groupedAreasItems = this._groupedAreasItems(
this.hass.areas,
this.hass.floors
);
const filteredFloors = this._sortedFloors(this.hass.floors).filter( const filteredFloors = this._sortedFloors(
this.hass.floors,
this.value?.floors_display?.order
).filter(
(floor) => (floor) =>
// Only include floors that have areas assigned to them // Only include floors that have areas assigned to them
groupedItems[floor.floor_id]?.length > 0 groupedAreasItems[floor.floor_id]?.length > 0
); );
const value: DisplayValue = { const value: DisplayValue = {
order: this.value?.order ?? [], order: this.value?.areas_display?.order ?? [],
hidden: this.value?.hidden ?? [], hidden: this.value?.areas_display?.hidden ?? [],
}; };
const canReorderFloors =
filteredFloors.filter((floor) => floor.floor_id !== UNASSIGNED_FLOOR)
.length > 1;
return html` return html`
<ha-expansion-panel ${this.label ? html`<label>${this.label}</label>` : nothing}
outlined <ha-sortable
.header=${this.label} draggable-selector=".draggable"
.expanded=${this.expanded} handle-selector=".handle"
@item-moved=${this._floorMoved}
.disabled=${this.disabled || !canReorderFloors}
> >
<ha-svg-icon slot="leading-icon" .path=${mdiTextureBox}></ha-svg-icon> <div>
${filteredFloors.map((floor, _, array) => { ${repeat(
const noFloors = filteredFloors,
array.length === 1 && floor.floor_id === UNASSIGNED_FLOOR; (floor) => floor.floor_id,
return html` (floor: FloorRegistryEntry) => html`
<div class="floor"> <ha-expansion-panel
${noFloors outlined
? nothing .header=${computeFloorName(floor)}
: html`<div class="header"> left-chevron
<ha-floor-icon .floor=${floor}></ha-floor-icon> class=${floor.floor_id === UNASSIGNED_FLOOR ? "" : "draggable"}
<p>${computeFloorName(floor)}</p> >
</div>`} <ha-floor-icon
<div class="areas"> slot="leading-icon"
.floor=${floor}
></ha-floor-icon>
${floor.floor_id === UNASSIGNED_FLOOR || !canReorderFloors
? nothing
: html`
<ha-svg-icon
class="handle"
slot="icons"
.path=${mdiDrag}
></ha-svg-icon>
`}
<ha-items-display-editor <ha-items-display-editor
.hass=${this.hass} .hass=${this.hass}
.items=${groupedItems[floor.floor_id] || []} .items=${groupedAreasItems[floor.floor_id]}
.value=${value} .value=${value}
.floorId=${floor.floor_id ?? UNASSIGNED_FLOOR} .floorId=${floor.floor_id}
@value-changed=${this._areaDisplayChanged} @value-changed=${this._areaDisplayChanged}
.showNavigationButton=${this.showNavigationButton} .showNavigationButton=${this.showNavigationButton}
></ha-items-display-editor> ></ha-items-display-editor>
</div> </ha-expansion-panel>
</div> `
`; )}
})} </div>
</ha-expansion-panel> </ha-sortable>
`; `;
} }
private _groupedItems = memoizeOne( private _groupedAreasItems = memoizeOne(
( (
hassAreas: HomeAssistant["areas"], hassAreas: HomeAssistant["areas"],
// update items if floors change // update items if floors change
@ -116,7 +143,6 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
label: area.name, label: area.name,
icon: area.icon ?? undefined, icon: area.icon ?? undefined,
iconPath: mdiTextureBox, iconPath: mdiTextureBox,
description: floor?.name,
}); });
return acc; return acc;
@ -128,18 +154,17 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
); );
private _sortedFloors = memoizeOne( private _sortedFloors = memoizeOne(
(hassFloors: HomeAssistant["floors"]): FloorRegistryEntry[] => { (
const floors = Object.values(hassFloors).sort((floorA, floorB) => { hassFloors: HomeAssistant["floors"],
if (floorA.level !== floorB.level) { order: string[] | undefined
return (floorA.level ?? 0) - (floorB.level ?? 0); ): FloorRegistryEntry[] => {
} const floors = getFloors(hassFloors, order);
return stringCompare(floorA.name, floorB.name); const noFloors = floors.length === 0;
});
floors.push({ floors.push({
floor_id: UNASSIGNED_FLOOR, floor_id: UNASSIGNED_FLOOR,
name: this.hass.localize( name: noFloors
"ui.panel.lovelace.strategy.areas.others_areas" ? this.hass.localize("ui.panel.lovelace.strategy.areas.areas")
), : this.hass.localize("ui.panel.lovelace.strategy.areas.other_areas"),
icon: null, icon: null,
level: null, level: null,
aliases: [], aliases: [],
@ -150,17 +175,43 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
} }
); );
private async _areaDisplayChanged(ev) { private _floorMoved(ev: CustomEvent<HASSDomEvents["item-moved"]>) {
ev.stopPropagation(); ev.stopPropagation();
const value = ev.detail.value as DisplayValue; const newIndex = ev.detail.newIndex;
const currentFloorId = ev.currentTarget.floorId; const oldIndex = ev.detail.oldIndex;
const floorIds = this._sortedFloors(
this.hass.floors,
this.value?.floors_display?.order
).map((floor) => floor.floor_id);
const newOrder = [...floorIds];
const movedFloorId = newOrder.splice(oldIndex, 1)[0];
newOrder.splice(newIndex, 0, movedFloorId);
const newValue: AreasFloorsDisplayValue = {
areas_display: this.value?.areas_display,
floors_display: {
order: newOrder,
},
};
if (newValue.floors_display?.order?.length === 0) {
delete newValue.floors_display.order;
}
fireEvent(this, "value-changed", { value: newValue });
}
const floorIds = this._sortedFloors(this.hass.floors).map( private async _areaDisplayChanged(ev: CustomEvent<{ value: DisplayValue }>) {
(floor) => floor.floor_id ev.stopPropagation();
); const value = ev.detail.value;
const currentFloorId = (ev.currentTarget as any).floorId;
const oldHidden = this.value?.hidden ?? []; const floorIds = this._sortedFloors(
const oldOrder = this.value?.order ?? []; this.hass.floors,
this.value?.floors_display?.order
).map((floor) => floor.floor_id);
const oldAreaDisplay = this.value?.areas_display ?? {};
const oldHidden = oldAreaDisplay?.hidden ?? [];
const oldOrder = oldAreaDisplay?.order ?? [];
const newHidden: string[] = []; const newHidden: string[] = [];
const newOrder: string[] = []; const newOrder: string[] = [];
@ -187,37 +238,27 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
} }
} }
const newValue: AreasDisplayValue = { const newValue: AreasFloorsDisplayValue = {
hidden: newHidden, areas_display: {
order: newOrder, hidden: newHidden,
order: newOrder,
},
floors_display: this.value?.floors_display,
}; };
if (newValue.hidden?.length === 0) { if (newValue.areas_display?.hidden?.length === 0) {
delete newValue.hidden; delete newValue.areas_display.hidden;
} }
if (newValue.order?.length === 0) { if (newValue.areas_display?.order?.length === 0) {
delete newValue.order; delete newValue.areas_display.order;
} }
this.value = newValue; if (newValue.floors_display?.order?.length === 0) {
delete newValue.floors_display.order;
}
fireEvent(this, "value-changed", { value: newValue }); fireEvent(this, "value-changed", { value: newValue });
} }
static styles = css` static styles = css`
.floor .header p {
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
flex: 1;
}
.floor .header {
margin: 16px 0 8px 0;
padding: 0 8px;
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
ha-expansion-panel { ha-expansion-panel {
margin-bottom: 8px; margin-bottom: 8px;
--expansion-panel-summary-padding: 0 16px; --expansion-panel-summary-padding: 0 16px;
@ -225,6 +266,11 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
ha-expansion-panel [slot="leading-icon"] { ha-expansion-panel [slot="leading-icon"] {
margin-inline-end: 16px; margin-inline-end: 16px;
} }
label {
display: block;
font-weight: var(--ha-font-weight-bold);
margin-bottom: 8px;
}
`; `;
} }

View File

@ -559,9 +559,9 @@ export class HaCodeEditor extends ReactiveElement {
right: 8px; right: 8px;
z-index: 1; z-index: 1;
color: var(--secondary-text-color); color: var(--secondary-text-color);
background-color: var(--card-background-color); background-color: var(--secondary-background-color);
border-radius: 50%; border-radius: 50%;
opacity: 0.6; opacity: 0.9;
transition: opacity 0.2s; transition: opacity 0.2s;
--mdc-icon-button-size: 32px; --mdc-icon-button-size: 32px;
--mdc-icon-size: 18px; --mdc-icon-size: 18px;
@ -591,7 +591,7 @@ export class HaCodeEditor extends ReactiveElement {
z-index: 9999 !important; z-index: 9999 !important;
background-color: var( background-color: var(
--code-editor-background-color, --code-editor-background-color,
var(--mdc-text-field-fill-color, whitesmoke) var(--card-background-color)
) !important; ) !important;
margin: 0 !important; margin: 0 !important;
padding-top: var(--safe-area-inset-top) !important; padding-top: var(--safe-area-inset-top) !important;

View File

@ -26,7 +26,6 @@ export class HaControlButtonGroup extends LitElement {
.container { .container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: var(--control-button-group-alignment, start);
width: 100%; width: 100%;
height: 100%; height: 100%;
} }

View File

@ -122,22 +122,6 @@ export class HaItemDisplayEditor extends LitElement {
${description ${description
? html`<span slot="supporting-text">${description}</span>` ? html`<span slot="supporting-text">${description}</span>`
: nothing} : nothing}
${isVisible && !disableSorting
? html`
<ha-svg-icon
tabindex=${ifDefined(
this.showNavigationButton ? "0" : undefined
)}
.idx=${idx}
@keydown=${this.showNavigationButton
? this._dragHandleKeydown
: undefined}
class="handle"
.path=${mdiDrag}
slot="start"
></ha-svg-icon>
`
: html`<ha-svg-icon slot="start"></ha-svg-icon>`}
${!showIcon ${!showIcon
? nothing ? nothing
: icon : icon
@ -162,6 +146,9 @@ export class HaItemDisplayEditor extends LitElement {
<span slot="end"> ${this.actionsRenderer(item)} </span> <span slot="end"> ${this.actionsRenderer(item)} </span>
` `
: nothing} : nothing}
${this.showNavigationButton
? html`<ha-icon-next slot="end"></ha-icon-next>`
: nothing}
<ha-icon-button <ha-icon-button
.path=${isVisible ? mdiEye : mdiEyeOff} .path=${isVisible ? mdiEye : mdiEyeOff}
slot="end" slot="end"
@ -174,9 +161,22 @@ export class HaItemDisplayEditor extends LitElement {
.value=${value} .value=${value}
@click=${this._toggle} @click=${this._toggle}
></ha-icon-button> ></ha-icon-button>
${this.showNavigationButton ${isVisible && !disableSorting
? html` <ha-icon-next slot="end"></ha-icon-next> ` ? html`
: nothing} <ha-svg-icon
tabindex=${ifDefined(
this.showNavigationButton ? "0" : undefined
)}
.idx=${idx}
@keydown=${this.showNavigationButton
? this._dragHandleKeydown
: undefined}
class="handle"
.path=${mdiDrag}
slot="end"
></ha-svg-icon>
`
: html`<ha-svg-icon slot="end"></ha-svg-icon>`}
</ha-md-list-item> </ha-md-list-item>
`; `;
} }

View File

@ -122,11 +122,7 @@ export class HaObjectSelector extends LitElement {
} }
protected render() { protected render() {
if (!this.selector.object) { if (this.selector.object?.fields) {
return nothing;
}
if (this.selector.object.fields) {
if (this.selector.object.multiple) { if (this.selector.object.multiple) {
const items = ensureArray(this.value ?? []); const items = ensureArray(this.value ?? []);
return html` return html`

View File

@ -89,6 +89,7 @@ export class HaSettingsRow extends LitElement {
display: var(--settings-row-content-display, flex); display: var(--settings-row-content-display, flex);
justify-content: flex-end; justify-content: flex-end;
flex: 1; flex: 1;
min-width: 0;
padding: 16px 0; padding: 16px 0;
} }
.content ::slotted(*) { .content ::slotted(*) {

View File

@ -173,6 +173,7 @@ export abstract class HaBlueprintGenericEditor extends LitElement {
.content=${value?.description} .content=${value?.description}
></ha-markdown> ></ha-markdown>
${html`<ha-selector ${html`<ha-selector
narrow
.hass=${this.hass} .hass=${this.hass}
.selector=${selector} .selector=${selector}
.key=${key} .key=${key}

View File

@ -1,9 +1,9 @@
import { import {
mdiCogOutline,
mdiDelete, mdiDelete,
mdiDevices, mdiDevices,
mdiDotsVertical, mdiDotsVertical,
mdiPencil, mdiPencil,
mdiShapeOutline,
mdiStopCircleOutline, mdiStopCircleOutline,
mdiTransitConnectionVariant, mdiTransitConnectionVariant,
} from "@mdi/js"; } from "@mdi/js";
@ -58,111 +58,118 @@ class HaConfigEntryDeviceRow extends LitElement {
area ? area.name : undefined, area ? area.name : undefined,
].filter(Boolean); ].filter(Boolean);
return html`<ha-md-list-item @click=${this.narrow ? this._handleNavigateToDevice : undefined} class=${classMap({ disabled: Boolean(device.disabled_by) })}> return html`<ha-md-list-item
<ha-svg-icon .path=${device.entry_type === "service" ? mdiTransitConnectionVariant : mdiDevices} slot="start"></ha-svg-icon> type="button"
<div slot="headline"></div>${computeDeviceNameDisplay(device, this.hass)}</div> @click=${this._handleNavigateToDevice}
class=${classMap({ disabled: Boolean(device.disabled_by) })}
>
<ha-svg-icon
.path=${device.entry_type === "service"
? mdiTransitConnectionVariant
: mdiDevices}
slot="start"
></ha-svg-icon>
<div slot="headline">${computeDeviceNameDisplay(device, this.hass)}</div>
<span slot="supporting-text" <span slot="supporting-text"
>${supportingText.join(" • ")} >${supportingText.join(" • ")}
${supportingText.length && entities.length ? " • " : nothing} ${supportingText.length && entities.length ? " • " : nothing}
${ ${entities.length
entities.length ? this.hass.localize(
? this.narrow "ui.panel.config.integrations.config_entry.entities",
? this.hass.localize( { count: entities.length }
"ui.panel.config.integrations.config_entry.entities", )
{ count: entities.length } : nothing}</span
)
: html`<a
href=${`/config/entities/?historyBack=1&device=${device.id}`}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.entities",
{ count: entities.length }
)}</a
>`
: nothing
}</span
> >
${ ${!this.narrow
!this.narrow ? html`<ha-icon-next slot="end"> </ha-icon-next>`
? html`<ha-icon-button-next : nothing}
slot="end"
@click=${this._handleNavigateToDevice}
>
</ha-icon-button-next>`
: nothing
}
</ha-icon-button>
<div class="vertical-divider" slot="end" @click=${stopPropagation}></div> <div class="vertical-divider" slot="end" @click=${stopPropagation}></div>
${ ${!this.narrow
!this.narrow ? html`<ha-icon-button
? html`<ha-icon-button slot="end"
slot="end" @click=${this._handleEditDevice}
@click=${this._handleConfigureDevice} .path=${mdiPencil}
.path=${mdiPencil} .label=${this.hass.localize(
.label=${this.hass.localize( "ui.panel.config.integrations.config_entry.device.edit"
"ui.panel.config.integrations.config_entry.device.configure" )}
)} ></ha-icon-button>`
></ha-icon-button>` : nothing}
: nothing
} <ha-md-button-menu
</ha-icon-button> positioning="popover"
<ha-md-button-menu positioning="popover" slot="end" @click=${stopPropagation}> slot="end"
@click=${stopPropagation}
>
<ha-icon-button <ha-icon-button
slot="trigger" slot="trigger"
.label=${this.hass.localize("ui.common.menu")} .label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical} .path=${mdiDotsVertical}
></ha-icon-button> ></ha-icon-button>
${ ${this.narrow
this.narrow ? html`<ha-md-menu-item @click=${this._handleEditDevice}>
? html`<ha-md-menu-item @click=${this._handleConfigureDevice}> <ha-svg-icon .path=${mdiPencil} slot="start"></ha-svg-icon>
<ha-svg-icon .path=${mdiCogOutline} slot="start"></ha-svg-icon> ${this.hass.localize(
${this.hass.localize( "ui.panel.config.integrations.config_entry.device.edit"
"ui.panel.config.integrations.config_entry.device.configure" )}
)} </ha-md-menu-item>`
</ha-md-menu-item>` : nothing}
: nothing ${entities.length
} ? html`
<ha-md-menu-item class=${device.disabled_by !== "user" ? "warning" : ""} @click=${this._handleDisableDevice} .disabled=${device.disabled_by !== "user" && device.disabled_by}> <ha-md-menu-item
<ha-svg-icon .path=${mdiStopCircleOutline} slot="start"></ha-svg-icon> href=${`/config/entities/?historyBack=1&device=${device.id}`}
${
device.disabled_by && device.disabled_by !== "user"
? this.hass.localize(
"ui.dialogs.device-registry-detail.enabled_cause",
{
type: this.hass.localize(
`ui.dialogs.device-registry-detail.type.${
device.entry_type || "device"
}`
),
cause: this.hass.localize(
`config_entry.disabled_by.${device.disabled_by}`
),
}
)
: device.disabled_by
? this.hass.localize(
"ui.panel.config.integrations.config_entry.device.enable"
)
: this.hass.localize(
"ui.panel.config.integrations.config_entry.device.disable"
)
}
</ha-md-menu-item>
${
this.entry.supports_remove_device
? html` <ha-md-menu-item
class="warning"
@click=${this._handleDeleteDevice}
> >
<ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon> <ha-svg-icon
.path=${mdiShapeOutline}
slot="start"
></ha-svg-icon>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.integrations.config_entry.device.delete" `ui.panel.config.integrations.config_entry.entities`,
{ count: entities.length }
)} )}
</ha-md-menu-item>` <ha-icon-next slot="end"></ha-icon-next>
: nothing </ha-md-menu-item>
} `
: nothing}
<ha-md-menu-item
class=${device.disabled_by !== "user" ? "warning" : ""}
@click=${this._handleDisableDevice}
.disabled=${device.disabled_by !== "user" && device.disabled_by}
>
<ha-svg-icon .path=${mdiStopCircleOutline} slot="start"></ha-svg-icon>
${device.disabled_by && device.disabled_by !== "user"
? this.hass.localize(
"ui.dialogs.device-registry-detail.enabled_cause",
{
type: this.hass.localize(
`ui.dialogs.device-registry-detail.type.${
device.entry_type || "device"
}`
),
cause: this.hass.localize(
`config_entry.disabled_by.${device.disabled_by}`
),
}
)
: device.disabled_by
? this.hass.localize(
"ui.panel.config.integrations.config_entry.device.enable"
)
: this.hass.localize(
"ui.panel.config.integrations.config_entry.device.disable"
)}
</ha-md-menu-item>
${this.entry.supports_remove_device
? html`<ha-md-menu-item
class="warning"
@click=${this._handleDeleteDevice}
>
<ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.device.delete"
)}
</ha-md-menu-item>`
: nothing}
</ha-md-button-menu> </ha-md-button-menu>
</ha-md-list-item> `; </ha-md-list-item> `;
} }
@ -170,7 +177,7 @@ class HaConfigEntryDeviceRow extends LitElement {
private _getEntities = (): EntityRegistryEntry[] => private _getEntities = (): EntityRegistryEntry[] =>
this.entities?.filter((entity) => entity.device_id === this.device.id); this.entities?.filter((entity) => entity.device_id === this.device.id);
private _handleConfigureDevice(ev: MouseEvent) { private _handleEditDevice(ev: MouseEvent) {
ev.stopPropagation(); // Prevent triggering the click handler on the list item ev.stopPropagation(); // Prevent triggering the click handler on the list item
showDeviceRegistryDetailDialog(this, { showDeviceRegistryDetailDialog(this, {
device: this.device, device: this.device,
@ -295,6 +302,8 @@ class HaConfigEntryDeviceRow extends LitElement {
} }
ha-md-list-item { ha-md-list-item {
--md-list-item-leading-space: 56px; --md-list-item-leading-space: 56px;
--md-ripple-hover-color: transparent;
--md-ripple-pressed-color: transparent;
} }
.disabled { .disabled {
opacity: 0.5; opacity: 0.5;

View File

@ -154,7 +154,10 @@ class HaConfigEntryRow extends LitElement {
statusLine.push( statusLine.push(
html`<a html`<a
href=${`/config/entities/?historyBack=1&config_entry=${item.entry_id}`} href=${`/config/entities/?historyBack=1&config_entry=${item.entry_id}`}
>${entities.length} entities</a >${this.hass.localize(
"ui.panel.config.integrations.config_entry.entities",
{ count: entities.length }
)}</a
>` >`
); );
} }

View File

@ -432,7 +432,8 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
)} )}
</div>` </div>`
: nothing} : nothing}
${this._manifest?.quality_scale && ${this._manifest?.is_built_in &&
this._manifest.quality_scale &&
Object.keys(QUALITY_SCALE_MAP).includes( Object.keys(QUALITY_SCALE_MAP).includes(
this._manifest.quality_scale this._manifest.quality_scale
) )

View File

@ -25,9 +25,6 @@ export const cardFeatureStyles = css`
flex-basis: 20px; flex-basis: 20px;
--control-button-padding: 0px; --control-button-padding: 0px;
} }
ha-control-button-group[no-stretch] > ha-control-button {
max-width: 48px;
}
ha-control-button { ha-control-button {
--control-button-focus-color: var(--feature-color); --control-button-focus-color: var(--feature-color);
} }

View File

@ -1,5 +1,6 @@
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { classMap } from "lit/directives/class-map";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@ -229,7 +230,11 @@ class HuiAreaControlsCardFeature
} }
return html` return html`
<ha-control-button-group ?no-stretch=${this.position === "inline"}> <ha-control-button-group
class=${classMap({
"no-stretch": this.position === "inline",
})}
>
${displayControls.map((control) => { ${displayControls.map((control) => {
const button = AREA_CONTROLS_BUTTONS[control]; const button = AREA_CONTROLS_BUTTONS[control];
@ -292,8 +297,22 @@ class HuiAreaControlsCardFeature
return [ return [
cardFeatureStyles, cardFeatureStyles,
css` css`
:host {
pointer-events: none !important;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
ha-control-button-group { ha-control-button-group {
--control-button-group-alignment: flex-end; pointer-events: auto;
width: 100%;
}
ha-control-button-group.no-stretch {
width: auto;
max-width: 100%;
}
ha-control-button-group.no-stretch > ha-control-button {
width: 48px;
} }
ha-control-button { ha-control-button {
--active-color: var(--state-active-color); --active-color: var(--state-active-color);

View File

@ -1,4 +1,4 @@
import { LitElement, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { HuiErrorCard } from "../cards/hui-error-card"; import type { HuiErrorCard } from "../cards/hui-error-card";
@ -56,6 +56,12 @@ export class HuiCardFeature extends LitElement {
} }
return html`${element}`; return html`${element}`;
} }
static styles = css`
:host > * {
pointer-events: auto;
}
`;
} }
declare global { declare global {

View File

@ -46,6 +46,7 @@ export class HuiCardFeatures extends LitElement {
--feature-height: 42px; --feature-height: 42px;
--feature-border-radius: 12px; --feature-border-radius: 12px;
--feature-button-spacing: 12px; --feature-button-spacing: 12px;
pointer-events: none;
position: relative; position: relative;
width: 100%; width: 100%;
display: flex; display: flex;

View File

@ -348,6 +348,14 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
return undefined; return undefined;
} }
// If only one entity, return its formatted state
if (entities.length === 1) {
const stateObj = entities[0];
return isUnavailableState(stateObj.state)
? ""
: this.hass.formatEntityState(stateObj);
}
// Use the first entity's unit_of_measurement for formatting // Use the first entity's unit_of_measurement for formatting
const uom = entities.find( const uom = entities.find(
(entity) => entity.attributes.unit_of_measurement (entity) => entity.attributes.unit_of_measurement

View File

@ -144,6 +144,9 @@ class DialogDashboardStrategyEditor extends LitElement {
.path=${mdiClose} .path=${mdiClose}
></ha-icon-button> ></ha-icon-button>
<span slot="title" .title=${title}>${title}</span> <span slot="title" .title=${title}>${title}</span>
${this._params.title
? html`<span slot="subtitle">${this._params.title}</span>`
: nothing}
<ha-button-menu <ha-button-menu
corner="BOTTOM_END" corner="BOTTOM_END"
menu-corner="END" menu-corner="END"

View File

@ -3,6 +3,7 @@ import type { LovelaceDashboardStrategyConfig } from "../../../../../data/lovela
export interface DashboardStrategyEditorDialogParams { export interface DashboardStrategyEditorDialogParams {
config: LovelaceDashboardStrategyConfig; config: LovelaceDashboardStrategyConfig;
title?: string;
saveConfig: (config: LovelaceDashboardStrategyConfig) => void; saveConfig: (config: LovelaceDashboardStrategyConfig) => void;
takeControl: () => void; takeControl: () => void;
deleteDashboard: () => Promise<boolean>; deleteDashboard: () => Promise<boolean>;

View File

@ -782,6 +782,7 @@ class HUIRoot extends LitElement {
showDashboardStrategyEditorDialog(this, { showDashboardStrategyEditorDialog(this, {
config: this.lovelace!.rawConfig, config: this.lovelace!.rawConfig,
title: this.panel ? getPanelTitle(this.hass, this.panel) : undefined,
saveConfig: this.lovelace!.saveConfig, saveConfig: this.lovelace!.saveConfig,
takeControl: () => { takeControl: () => {
showSaveDialog(this, { showSaveDialog(this, {

View File

@ -22,6 +22,9 @@ export interface AreasDashboardStrategyConfig {
hidden?: string[]; hidden?: string[];
order?: string[]; order?: string[];
}; };
floors_display?: {
order?: string[];
};
areas_options?: Record<string, AreaOptions>; areas_options?: Record<string, AreaOptions>;
} }
@ -84,6 +87,7 @@ export class AreasDashboardStrategy extends ReactiveElement {
type: "areas-overview", type: "areas-overview",
areas_display: config.areas_display, areas_display: config.areas_display,
areas_options: config.areas_options, areas_options: config.areas_options,
floors_display: config.floors_display,
} satisfies AreasViewStrategyConfig, } satisfies AreasViewStrategyConfig,
}, },
...areaViews, ...areaViews,

View File

@ -1,6 +1,5 @@
import { ReactiveElement } from "lit"; import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { stringCompare } from "../../../../common/string/compare";
import { floorDefaultIcon } from "../../../../components/ha-floor-icon"; import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section"; import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view"; import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
@ -9,7 +8,11 @@ import { getAreaControlEntities } from "../../card-features/hui-area-controls-ca
import { AREA_CONTROLS, type AreaControl } from "../../card-features/types"; import { AREA_CONTROLS, type AreaControl } from "../../card-features/types";
import type { AreaCardConfig, HeadingCardConfig } from "../../cards/types"; import type { AreaCardConfig, HeadingCardConfig } from "../../cards/types";
import type { EntitiesDisplay } from "./area-view-strategy"; import type { EntitiesDisplay } from "./area-view-strategy";
import { computeAreaPath, getAreas } from "./helpers/areas-strategy-helper"; import {
computeAreaPath,
getAreas,
getFloors,
} from "./helpers/areas-strategy-helper";
const UNASSIGNED_FLOOR = "__unassigned__"; const UNASSIGNED_FLOOR = "__unassigned__";
@ -23,6 +26,9 @@ export interface AreasViewStrategyConfig {
hidden?: string[]; hidden?: string[];
order?: string[]; order?: string[];
}; };
floors_display?: {
order?: string[];
};
areas_options?: Record<string, AreaOptions>; areas_options?: Record<string, AreaOptions>;
} }
@ -38,19 +44,13 @@ export class AreasOverviewViewStrategy extends ReactiveElement {
config.areas_display?.order config.areas_display?.order
); );
const floors = Object.values(hass.floors); const floors = getFloors(hass.floors, config.floors_display?.order);
floors.sort((floorA, floorB) => {
if (floorA.level !== floorB.level) {
return (floorA.level ?? 0) - (floorB.level ?? 0);
}
return stringCompare(floorA.name, floorB.name);
});
const floorSections = [ const floorSections = [
...floors, ...floors,
{ {
floor_id: UNASSIGNED_FLOOR, floor_id: UNASSIGNED_FLOOR,
name: hass.localize("ui.panel.lovelace.strategy.areas.others_areas"), name: hass.localize("ui.panel.lovelace.strategy.areas.other_areas"),
level: null, level: null,
icon: null, icon: null,
}, },

View File

@ -1,10 +1,12 @@
import { mdiThermometerWater } from "@mdi/js"; import { mdiThermometerWater } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-areas-display-editor"; import "../../../../../components/ha-areas-display-editor";
import type { AreasDisplayValue } from "../../../../../components/ha-areas-display-editor"; import type { AreasDisplayValue } from "../../../../../components/ha-areas-display-editor";
import "../../../../../components/ha-areas-floors-display-editor"; import "../../../../../components/ha-areas-floors-display-editor";
import type { AreasFloorsDisplayValue } from "../../../../../components/ha-areas-floors-display-editor";
import "../../../../../components/ha-entities-display-editor"; import "../../../../../components/ha-entities-display-editor";
import "../../../../../components/ha-icon"; import "../../../../../components/ha-icon";
import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-icon-button";
@ -126,7 +128,7 @@ export class HuiAreasDashboardStrategyEditor
`; `;
} }
const value = this._config.areas_display; const value = this._areasFloorsDisplayValue(this._config);
return html` return html`
<ha-areas-floors-display-editor <ha-areas-floors-display-editor
@ -135,7 +137,7 @@ export class HuiAreasDashboardStrategyEditor
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.lovelace.editor.strategy.areas.areas_display" "ui.panel.lovelace.editor.strategy.areas.areas_display"
)} )}
@value-changed=${this._areasDisplayChanged} @value-changed=${this._areasFloorsDisplayChanged}
expanded expanded
show-navigation-button show-navigation-button
@item-display-navigate-clicked=${this._handleAreaNavigate} @item-display-navigate-clicked=${this._handleAreaNavigate}
@ -149,6 +151,13 @@ export class HuiAreasDashboardStrategyEditor
} }
} }
private _areasFloorsDisplayValue = memoizeOne(
(config: AreasDashboardStrategyConfig): AreasFloorsDisplayValue => ({
areas_display: config.areas_display,
floors_display: config.floors_display,
})
);
private _editArea(ev: Event): void { private _editArea(ev: Event): void {
ev.stopPropagation(); ev.stopPropagation();
const area = (ev.currentTarget! as any).area as AreaRegistryEntry; const area = (ev.currentTarget! as any).area as AreaRegistryEntry;
@ -163,11 +172,11 @@ export class HuiAreasDashboardStrategyEditor
this._area = ev.detail.value; this._area = ev.detail.value;
} }
private _areasDisplayChanged(ev: CustomEvent): void { private _areasFloorsDisplayChanged(ev: CustomEvent): void {
const value = ev.detail.value as AreasDisplayValue; const value = ev.detail.value as AreasFloorsDisplayValue;
const newConfig: AreasDashboardStrategyConfig = { const newConfig: AreasDashboardStrategyConfig = {
...this._config!, ...this._config!,
areas_display: value, ...value,
}; };
fireEvent(this, "config-changed", { config: newConfig }); fireEvent(this, "config-changed", { config: newConfig });

View File

@ -3,9 +3,13 @@ import { computeStateName } from "../../../../../common/entity/compute_state_nam
import type { EntityFilterFunc } from "../../../../../common/entity/entity_filter"; import type { EntityFilterFunc } from "../../../../../common/entity/entity_filter";
import { generateEntityFilter } from "../../../../../common/entity/entity_filter"; import { generateEntityFilter } from "../../../../../common/entity/entity_filter";
import { stripPrefixFromEntityName } from "../../../../../common/entity/strip_prefix_from_entity_name"; import { stripPrefixFromEntityName } from "../../../../../common/entity/strip_prefix_from_entity_name";
import { orderCompare } from "../../../../../common/string/compare"; import {
orderCompare,
stringCompare,
} from "../../../../../common/string/compare";
import type { AreaRegistryEntry } from "../../../../../data/area_registry"; import type { AreaRegistryEntry } from "../../../../../data/area_registry";
import { areaCompare } 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 { LovelaceCardConfig } from "../../../../../data/lovelace/config/card";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import { supportsAlarmModesCardFeature } from "../../../card-features/hui-alarm-modes-card-feature"; import { supportsAlarmModesCardFeature } from "../../../card-features/hui-alarm-modes-card-feature";
@ -290,4 +294,23 @@ export const getAreas = (
return sortedAreas; return sortedAreas;
}; };
export const getFloors = (
entries: HomeAssistant["floors"],
floorsOrder?: string[]
): FloorRegistryEntry[] => {
const floors = Object.values(entries);
const compare = orderCompare(floorsOrder || []);
return floors.sort((floorA, floorB) => {
const order = compare(floorA.floor_id, floorB.floor_id);
if (order !== 0) {
return order;
}
if (floorA.level !== floorB.level) {
return (floorA.level ?? 0) - (floorB.level ?? 0);
}
return stringCompare(floorA.name, floorB.name);
});
};
export const computeAreaPath = (areaId: string): string => `areas-${areaId}`; export const computeAreaPath = (areaId: string): string => `areas-${areaId}`;

View File

@ -51,7 +51,7 @@ export const haTheme = EditorView.theme({
"&": { "&": {
color: "var(--primary-text-color)", color: "var(--primary-text-color)",
backgroundColor: backgroundColor:
"var(--code-editor-background-color, var(--mdc-text-field-fill-color, whitesmoke))", "var(--code-editor-background-color, var(--card-background-color))",
borderRadius: borderRadius:
"var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0px 0px", "var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0px 0px",
caretColor: "var(--secondary-text-color)", caretColor: "var(--secondary-text-color)",

View File

@ -51,7 +51,7 @@
"owner": "Owner", "owner": "Owner",
"system-admin": "Administrators", "system-admin": "Administrators",
"system-users": "Users", "system-users": "Users",
"system-read-only": "Read-Only Users" "system-read-only": "Read-only users"
}, },
"config_entry": { "config_entry": {
"disabled_by": { "disabled_by": {
@ -471,7 +471,7 @@
"radius_meters": "[%key:ui::panel::config::core::section::core::core_config::elevation_meters%]" "radius_meters": "[%key:ui::panel::config::core::section::core::core_config::elevation_meters%]"
}, },
"selector": { "selector": {
"options": "Selector Options", "options": "Selector options",
"types": { "types": {
"action": "Action", "action": "Action",
"area": "Area", "area": "Area",
@ -480,7 +480,7 @@
"color_temp": "Color temperature", "color_temp": "Color temperature",
"condition": "Condition", "condition": "Condition",
"date": "Date", "date": "Date",
"datetime": "Date and Time", "datetime": "Date and time",
"device": "Device", "device": "Device",
"duration": "Duration", "duration": "Duration",
"entity": "Entity", "entity": "Entity",
@ -490,7 +490,7 @@
"media": "Media", "media": "Media",
"number": "Number", "number": "Number",
"object": "Object", "object": "Object",
"color_rgb": "RGB Color", "color_rgb": "RGB color",
"select": "Select", "select": "Select",
"state": "State", "state": "State",
"target": "Target", "target": "Target",
@ -498,7 +498,7 @@
"text": "Text", "text": "Text",
"theme": "Theme", "theme": "Theme",
"time": "Time", "time": "Time",
"manual": "Manual Entry" "manual": "Manual entry"
} }
}, },
"template": { "template": {
@ -857,7 +857,7 @@
"cyan": "Cyan", "cyan": "Cyan",
"teal": "Teal", "teal": "Teal",
"green": "Green", "green": "Green",
"light-green": "Light Green", "light-green": "Light green",
"lime": "Lime", "lime": "Lime",
"yellow": "Yellow", "yellow": "Yellow",
"amber": "Amber", "amber": "Amber",
@ -1045,7 +1045,7 @@
"podcast": "Podcast", "podcast": "Podcast",
"season": "Season", "season": "Season",
"track": "Track", "track": "Track",
"tv_show": "TV Show", "tv_show": "TV show",
"url": "URL", "url": "URL",
"video": "Video" "video": "Video"
}, },
@ -2935,7 +2935,7 @@
"name": "Name", "name": "Name",
"description": "Description", "description": "Description",
"tag_id": "Tag ID", "tag_id": "Tag ID",
"tag_id_placeholder": "Autogenerated if left empty", "tag_id_placeholder": "Auto-generated if left empty",
"delete": "Delete", "delete": "Delete",
"update": "Update", "update": "Update",
"create": "Create", "create": "Create",
@ -4490,14 +4490,14 @@
"trace_no_longer_available": "Chosen trace is no longer available", "trace_no_longer_available": "Chosen trace is no longer available",
"enter_downloaded_trace": "Enter downloaded trace", "enter_downloaded_trace": "Enter downloaded trace",
"tabs": { "tabs": {
"details": "Step Details", "details": "Step details",
"timeline": "Trace Timeline", "timeline": "Trace timeline",
"logbook": "Related logbook entries", "logbook": "Related logbook entries",
"automation_config": "Automation Config", "automation_config": "Automation config",
"step_config": "Step Config", "step_config": "Step config",
"changed_variables": "Changed Variables", "changed_variables": "Changed variables",
"blueprint_config": "Blueprint Config", "blueprint_config": "Blueprint config",
"script_config": "Script Config" "script_config": "Script config"
}, },
"path": { "path": {
"choose": "Select a step on the left for more information.", "choose": "Select a step on the left for more information.",
@ -4544,7 +4544,7 @@
"caption": "Blueprints", "caption": "Blueprints",
"description": "Manage blueprints", "description": "Manage blueprints",
"overview": { "overview": {
"header": "Blueprint Editor", "header": "Blueprint editor",
"introduction": "The blueprint configuration allows you to import and manage your blueprints.", "introduction": "The blueprint configuration allows you to import and manage your blueprints.",
"learn_more": "Learn more about using blueprints", "learn_more": "Learn more about using blueprints",
"headers": { "headers": {
@ -4601,14 +4601,14 @@
"override_description": "Importing it will override the existing blueprint. If the updated blueprint is not compatible, it can break your automations. Automations will have to be adjusted manually.", "override_description": "Importing it will override the existing blueprint. If the updated blueprint is not compatible, it can break your automations. Automations will have to be adjusted manually.",
"error_no_url": "Please enter the blueprint address.", "error_no_url": "Please enter the blueprint address.",
"unsupported_blueprint": "This blueprint is not supported", "unsupported_blueprint": "This blueprint is not supported",
"file_name": "Blueprint Path" "file_name": "Blueprint path"
} }
}, },
"script": { "script": {
"caption": "Scripts", "caption": "Scripts",
"description": "Execute a sequence of actions", "description": "Execute a sequence of actions",
"picker": { "picker": {
"header": "Script Editor", "header": "Script editor",
"introduction": "The script editor allows you to create and edit scripts. Please follow the link below to read the instructions to make sure that you have configured Home Assistant correctly.", "introduction": "The script editor allows you to create and edit scripts. Please follow the link below to read the instructions to make sure that you have configured Home Assistant correctly.",
"learn_more": "Learn more about scripts", "learn_more": "Learn more about scripts",
"no_scripts": "We couldn't find any scripts", "no_scripts": "We couldn't find any scripts",
@ -4684,7 +4684,7 @@
"field_delete_confirm_title": "Delete field?", "field_delete_confirm_title": "Delete field?",
"field_delete_confirm_text": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm_text%]", "field_delete_confirm_text": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm_text%]",
"header": "Script: {name}", "header": "Script: {name}",
"default_name": "New Script", "default_name": "New script",
"modes": { "modes": {
"label": "[%key:ui::panel::config::automation::editor::modes::label%]", "label": "[%key:ui::panel::config::automation::editor::modes::label%]",
"learn_more": "[%key:ui::panel::config::automation::editor::modes::learn_more%]", "learn_more": "[%key:ui::panel::config::automation::editor::modes::learn_more%]",
@ -4729,7 +4729,7 @@
"description": "Capture device states and easily recall them later", "description": "Capture device states and easily recall them later",
"activated": "Activated scene {name}.", "activated": "Activated scene {name}.",
"picker": { "picker": {
"header": "Scene Editor", "header": "Scene editor",
"introduction": "The scene editor allows you to create and edit scenes. Please follow the link below to read the instructions to make sure that you have configured Home Assistant correctly.", "introduction": "The scene editor allows you to create and edit scenes. Please follow the link below to read the instructions to make sure that you have configured Home Assistant correctly.",
"learn_more": "Learn more about scenes", "learn_more": "Learn more about scenes",
"pick_scene": "Pick scene to edit", "pick_scene": "Pick scene to edit",
@ -4993,7 +4993,7 @@
"other_home_assistant": "Other Home Assistant", "other_home_assistant": "Other Home Assistant",
"instance_name": "Instance name", "instance_name": "Instance name",
"instance_version": "Instance version", "instance_version": "Instance version",
"ip_address": "IP Address", "ip_address": "IP address",
"connected_at": "Connected at", "connected_at": "Connected at",
"obfuscated_ip": { "obfuscated_ip": {
"show": "Show IP address", "show": "Show IP address",
@ -5383,6 +5383,7 @@
"confirm_disable_title": "Disable device?", "confirm_disable_title": "Disable device?",
"confirm_disable_message": "Are you sure you want to disable {name} and all of its entities?", "confirm_disable_message": "Are you sure you want to disable {name} and all of its entities?",
"configure": "Configure device", "configure": "Configure device",
"edit": "Edit device",
"delete": "Remove device" "delete": "Remove device"
}, },
"devices": "{count} {count, plural,\n one {device}\n other {devices}\n}", "devices": "{count} {count, plural,\n one {device}\n other {devices}\n}",
@ -5661,9 +5662,9 @@
}, },
"dhcp": { "dhcp": {
"title": "DHCP discovery", "title": "DHCP discovery",
"mac_address": "MAC Address", "mac_address": "MAC address",
"hostname": "Hostname", "hostname": "Hostname",
"ip_address": "IP Address", "ip_address": "IP address",
"no_devices_found": "No recent DHCP requests found; no matching discoveries detected" "no_devices_found": "No recent DHCP requests found; no matching discoveries detected"
}, },
"thread": { "thread": {
@ -5722,7 +5723,7 @@
"name": "Name", "name": "Name",
"type": "Type", "type": "Type",
"port": "Port", "port": "Port",
"ip_addresses": "IP Addresses", "ip_addresses": "IP addresses",
"properties": "Properties", "properties": "Properties",
"discovery_information": "Discovery information", "discovery_information": "Discovery information",
"copy_to_clipboard": "Copy to clipboard", "copy_to_clipboard": "Copy to clipboard",
@ -6354,7 +6355,7 @@
"title": "Door lock", "title": "Door lock",
"twist_assist": "Twist assist", "twist_assist": "Twist assist",
"block_to_block": "Block to block", "block_to_block": "Block to block",
"auto_relock_time": "Auto relock time", "auto_relock_time": "Autorelock time",
"hold_release_time": "Hold and release time", "hold_release_time": "Hold and release time",
"operation_type": "Operation type", "operation_type": "Operation type",
"operation_types": { "operation_types": {
@ -6525,13 +6526,13 @@
"prefix": "Subnet prefix", "prefix": "Subnet prefix",
"add_address": "Add address", "add_address": "Add address",
"gateway": "Gateway address", "gateway": "Gateway address",
"dns_server": "DNS Server", "dns_server": "DNS server",
"add_dns_server": "Add DNS Server", "add_dns_server": "Add DNS server",
"custom_dns": "Custom", "custom_dns": "Custom",
"unsaved": "You have unsaved changes, these will get lost if you change tabs, do you want to continue?", "unsaved": "You have unsaved changes, these will get lost if you change tabs, do you want to continue?",
"failed_to_change": "Failed to change network settings", "failed_to_change": "Failed to change network settings",
"hostname": { "hostname": {
"title": "Host name", "title": "Hostname",
"description": "The name your instance will have on your network", "description": "The name your instance will have on your network",
"failed_to_set_hostname": "Setting hostname failed" "failed_to_set_hostname": "Setting hostname failed"
} }
@ -6548,9 +6549,9 @@
}, },
"network_adapter": "Network adapter", "network_adapter": "Network adapter",
"network_adapter_info": "Configure which network adapters integrations will use. A restart is required for these settings to apply.", "network_adapter_info": "Configure which network adapters integrations will use. A restart is required for these settings to apply.",
"ip_information": "IP Information", "ip_information": "IP information",
"adapter": { "adapter": {
"auto_configure": "Auto configure", "auto_configure": "Autoconfigure",
"detected": "Detected", "detected": "Detected",
"adapter": "Adapter" "adapter": "Adapter"
} }
@ -6559,7 +6560,7 @@
"caption": "Storage", "caption": "Storage",
"description": "{percent_used} used - {free_space} free", "description": "{percent_used} used - {free_space} free",
"used_space": "Used space", "used_space": "Used space",
"emmc_lifetime_used": "eMMC Lifetime Used", "emmc_lifetime_used": "eMMC lifetime used",
"disk_metrics": "Disk metrics", "disk_metrics": "Disk metrics",
"datadisk": { "datadisk": {
"title": "Move data disk", "title": "Move data disk",
@ -6685,7 +6686,7 @@
"actions": "Actions", "actions": "Actions",
"others": "Others" "others": "Others"
}, },
"others_areas": "Other areas", "other_areas": "Other areas",
"areas": "Areas" "areas": "Areas"
} }
}, },
@ -6910,7 +6911,7 @@
}, },
"edit_view": { "edit_view": {
"header": "View configuration", "header": "View configuration",
"header_name": "{name} View Configuration", "header_name": "{name} view configuration",
"add": "Add view", "add": "Add view",
"background": { "background": {
"settings": "Background settings", "settings": "Background settings",
@ -7026,7 +7027,7 @@
}, },
"edit_card": { "edit_card": {
"header": "Card configuration", "header": "Card configuration",
"typed_header": "{type} Card configuration", "typed_header": "{type} card configuration",
"pick_card": "Add to dashboard", "pick_card": "Add to dashboard",
"pick_card_title": "Which card would you like to add to {name}", "pick_card_title": "Which card would you like to add to {name}",
"toggle_editor": "Toggle editor", "toggle_editor": "Toggle editor",
@ -7096,7 +7097,7 @@
"move_card": { "move_card": {
"header": "Choose a view to move the card to", "header": "Choose a view to move the card to",
"strategy_error_title": "Impossible to move the card", "strategy_error_title": "Impossible to move the card",
"strategy_error_text_strategy": "Moving a card to an auto generated view is not supported.", "strategy_error_text_strategy": "Moving a card to an auto-generated view is not supported.",
"success": "Card moved successfully", "success": "Card moved successfully",
"error": "Error while moving card" "error": "Error while moving card"
}, },
@ -7431,7 +7432,7 @@
}, },
"light": { "light": {
"name": "Light", "name": "Light",
"description": "The Light card allows you to change the brightness of the light." "description": "The Light card allows you to change the brightness of a light."
}, },
"generic": { "generic": {
"alt_text": "Alternative text", "alt_text": "Alternative text",
@ -7519,13 +7520,13 @@
"geo_location_sources": "Geolocation sources", "geo_location_sources": "Geolocation sources",
"no_geo_location_sources": "No geolocation sources available", "no_geo_location_sources": "No geolocation sources available",
"appearance": "Appearance", "appearance": "Appearance",
"theme_mode": "Theme Mode", "theme_mode": "Theme mode",
"theme_modes": { "theme_modes": {
"auto": "Auto", "auto": "Auto",
"light": "Light", "light": "Light",
"dark": "Dark" "dark": "Dark"
}, },
"default_zoom": "Default Zoom", "default_zoom": "Default zoom",
"source": "Source", "source": "Source",
"description": "The Map card that allows you to display entities on a map." "description": "The Map card that allows you to display entities on a map."
}, },
@ -7573,7 +7574,7 @@
"picture-elements": { "picture-elements": {
"name": "Picture elements", "name": "Picture elements",
"description": "The Picture elements card is one of the most versatile types of cards. The cards allow you to position icons or text and even actions! On an image based on coordinates.", "description": "The Picture elements card is one of the most versatile types of cards. The cards allow you to position icons or text and even actions! On an image based on coordinates.",
"card_options": "Card Options", "card_options": "Card options",
"elements": "Elements", "elements": "Elements",
"new_element": "Add new element", "new_element": "Add new element",
"confirm_delete_element": "Are you sure you want to delete the {type} element?", "confirm_delete_element": "Are you sure you want to delete the {type} element?",
@ -7612,7 +7613,7 @@
"none": "None", "none": "None",
"line": "Line" "line": "Line"
}, },
"description": "The Sensor card gives you a quick overview of your sensors state with an optional graph to visualize change over time.", "description": "The Sensor card gives you a quick overview of a sensor's state with an optional graph to visualize change over time.",
"limit_min": "Minimum value", "limit_min": "Minimum value",
"limit_max": "Maximum value" "limit_max": "Maximum value"
}, },
@ -7622,14 +7623,14 @@
"integration_not_loaded": "This card requires the `todo` integration to be set up.", "integration_not_loaded": "This card requires the `todo` integration to be set up.",
"hide_completed": "Hide completed items", "hide_completed": "Hide completed items",
"hide_create": "Hide 'Add item' field", "hide_create": "Hide 'Add item' field",
"display_order": "Display Order", "display_order": "Display order",
"sort_modes": { "sort_modes": {
"none": "Default", "none": "Default",
"manual": "Manual", "manual": "Manual",
"alpha_asc": "Alphabetical (A-Z)", "alpha_asc": "Alphabetical (A-Z)",
"alpha_desc": "Alphabetical (Z-A)", "alpha_desc": "Alphabetical (Z-A)",
"duedate_asc": "Due Date (Soonest First)", "duedate_asc": "Due date (Soonest first)",
"duedate_desc": "Due Date (Latest First)" "duedate_desc": "Due date (Latest first)"
} }
}, },
"thermostat": { "thermostat": {
@ -7639,7 +7640,7 @@
}, },
"tile": { "tile": {
"name": "Tile", "name": "Tile",
"description": "The tile card gives you a quick overview of your entity. The card allows you to toggle the entity, show the More info dialog or trigger custom actions.", "description": "The Tile card gives you a quick overview of an entity. The card allows you to toggle the entity, show the More info dialog or trigger custom actions.",
"color": "Color", "color": "Color",
"color_helper": "Inactive state (e.g. off, closed) will not be colored.", "color_helper": "Inactive state (e.g. off, closed) will not be colored.",
"icon_tap_action": "Icon tap behavior", "icon_tap_action": "Icon tap behavior",
@ -7693,7 +7694,7 @@
"badge": { "badge": {
"entity": { "entity": {
"name": "Entity", "name": "Entity",
"description": "The Entity badge gives you a quick overview of your entity.", "description": "The Entity badge gives you a quick overview of an entity.",
"color": "[%key:ui::panel::lovelace::editor::card::tile::color%]", "color": "[%key:ui::panel::lovelace::editor::card::tile::color%]",
"color_helper": "[%key:ui::panel::lovelace::editor::card::tile::color_helper%]", "color_helper": "[%key:ui::panel::lovelace::editor::card::tile::color_helper%]",
"show_entity_picture": "Show entity picture", "show_entity_picture": "Show entity picture",
@ -8231,14 +8232,14 @@
"confirm_delete_title": "Delete long-lived access token?", "confirm_delete_title": "Delete long-lived access token?",
"confirm_delete_text": "Are you sure you want to delete the long-lived access token for {name}?", "confirm_delete_text": "Are you sure you want to delete the long-lived access token for {name}?",
"delete_failed": "Failed to delete the access token.", "delete_failed": "Failed to delete the access token.",
"create": "Create Token", "create": "Create token",
"create_failed": "Failed to create the access token.", "create_failed": "Failed to create the access token.",
"name": "Name", "name": "Name",
"prompt_name": "Give the token a name", "prompt_name": "Give the token a name",
"prompt_copy_token": "Copy your access token. It will not be shown again.", "prompt_copy_token": "Copy your access token. It will not be shown again.",
"empty_state": "You have no long-lived access tokens yet.", "empty_state": "You have no long-lived access tokens yet.",
"qr_code_image": "QR code for token {name}", "qr_code_image": "QR code for token {name}",
"generate_qr_code": "Generate QR Code" "generate_qr_code": "Generate QR code"
} }
}, },
"todo": { "todo": {
@ -8395,7 +8396,7 @@
"hdmi_input": "HDMI input", "hdmi_input": "HDMI input",
"hdmi_switcher": "HDMI switcher", "hdmi_switcher": "HDMI switcher",
"volume": "Volume", "volume": "Volume",
"total_tv_time": "Total TV Time", "total_tv_time": "Total TV time",
"turn_tv_off": "Turn television off", "turn_tv_off": "Turn television off",
"air": "Air" "air": "Air"
}, },
@ -8665,7 +8666,7 @@
"input_button": "Input buttons", "input_button": "Input buttons",
"input_text": "Input texts", "input_text": "Input texts",
"input_number": "Input numbers", "input_number": "Input numbers",
"input_datetime": "Input date times", "input_datetime": "Input datetimes",
"input_select": "Input selects", "input_select": "Input selects",
"template": "Template entities", "template": "Template entities",
"universal": "Universal media player entities", "universal": "Universal media player entities",
@ -9076,7 +9077,7 @@
}, },
"capability": { "capability": {
"stage": { "stage": {
"title": "Add-on Stage", "title": "Add-on stage",
"description": "Add-ons can have one of three stages:\n\n{icon_stable} **Stable**: These are add-ons ready to be used in production.\n\n{icon_experimental} **Experimental**: These may contain bugs, and may be unfinished.\n\n{icon_deprecated} **Deprecated**: These add-ons will no longer receive any updates." "description": "Add-ons can have one of three stages:\n\n{icon_stable} **Stable**: These are add-ons ready to be used in production.\n\n{icon_experimental} **Experimental**: These may contain bugs, and may be unfinished.\n\n{icon_deprecated} **Deprecated**: These add-ons will no longer receive any updates."
}, },
"rating": { "rating": {
@ -9158,8 +9159,8 @@
"description": "This will restart the add-on if it crashes" "description": "This will restart the add-on if it crashes"
}, },
"auto_update": { "auto_update": {
"title": "Auto update", "title": "Autoupdate",
"description": "Auto update the add-on when there is a new version available" "description": "Autoupdate the add-on when there is a new version available"
}, },
"ingress_panel": { "ingress_panel": {
"title": "Show in sidebar", "title": "Show in sidebar",
@ -9270,7 +9271,7 @@
"addons": "Add-ons", "addons": "Add-ons",
"dashboard": "Dashboard", "dashboard": "Dashboard",
"backups": "Backups", "backups": "Backups",
"store": "Add-on Store", "store": "Add-on store",
"system": "System" "system": "System"
}, },
"my": { "my": {
@ -9360,7 +9361,7 @@
"hostname": "Hostname", "hostname": "Hostname",
"change_hostname": "Change hostname", "change_hostname": "Change hostname",
"new_hostname": "Please enter a new hostname:", "new_hostname": "Please enter a new hostname:",
"ip_address": "IP Address", "ip_address": "IP address",
"change": "Change", "change": "Change",
"operating_system": "Operating system", "operating_system": "Operating system",
"docker_version": "Docker version", "docker_version": "Docker version",
@ -9412,7 +9413,7 @@
"confirm_password": "Confirm encryption key", "confirm_password": "Confirm encryption key",
"password_protection": "Password protection", "password_protection": "Password protection",
"enter_password": "Please enter a password.", "enter_password": "Please enter a password.",
"passwords_not_matching": "The passwords does not match", "passwords_not_matching": "The passwords do not match",
"backup_already_running": "A backup or restore is already running. Creating a new backup is currently not possible, try again later.", "backup_already_running": "A backup or restore is already running. Creating a new backup is currently not possible, try again later.",
"confirm_restore_partial_backup_title": "Restore partial backup", "confirm_restore_partial_backup_title": "Restore partial backup",
"confirm_restore_partial_backup_text": "The backup will be restored. Depending on the size of the backup, this can take up to 45 min. Home Assistant needs to shutdown and the restore progress is running in the background. If it succeeds, Home Assistant will automatically start again.", "confirm_restore_partial_backup_text": "The backup will be restored. Depending on the size of the backup, this can take up to 45 min. Home Assistant needs to shutdown and the restore progress is running in the background. If it succeeds, Home Assistant will automatically start again.",