Compare commits

...

37 Commits

Author SHA1 Message Date
Bram Kragten
2ba8f9f99d Bumped version to 20250331.0 2025-03-31 20:43:31 +02:00
Bram Kragten
7e06bbc467 Fix add zwave device my link (#24871) 2025-03-31 20:42:38 +02:00
Paul Bottein
6017d82c21 Handle date range shift during daylight saving time days (#24868) 2025-03-31 20:42:37 +02:00
Bram Kragten
40c200a172 fix spinner in tts try dialog (#24867) 2025-03-31 20:42:36 +02:00
Bram Kragten
a2f70f682f Take lang into account when search existing pipeline (#24866)
* Take lang into account when search existing pipeline

* Simplify logic
2025-03-31 20:42:36 +02:00
Paul Bottein
c42a899b52 Force clock card to display time LTR (#24865) 2025-03-31 20:42:35 +02:00
Paul Bottein
706f43e99e Add interactions for weather card editor (#24864) 2025-03-31 20:42:34 +02:00
karwosts
f5496c21e8 Bar charts start from 0 (#24854) 2025-03-31 20:42:33 +02:00
Paul Bottein
34dce5b279 Only use button for breadcrumb for admin users (#24836) 2025-03-31 20:42:32 +02:00
Bram Kragten
a4f07423ec Name local pipeline based on local or full choice (#24835) 2025-03-31 20:42:31 +02:00
Bram Kragten
9e32c24f3c Update lang support text in voice wizard (#24834) 2025-03-31 20:42:30 +02:00
Paul Bottein
b281d095cd Remove add-on word in satellite wizard translations for state (#24832) 2025-03-31 20:42:29 +02:00
Paul Bottein
fe7e8e17ae More info breadcrumb clickable (#24830)
* Make more info breadcrum clickable

* css adjustements
2025-03-28 15:37:27 +01:00
Eloy Rodriguez
2161357226 Add title and time zone to clock card (#24818)
* Add title and time zone to clock card

* Small changes to the spacing and text sizing of the clock card

* Update translations to use dropdown labels from profile configuration

* Use similar approach as #24819 for setting automatic time zone

* Update hui-clock-card.ts

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-03-28 15:37:02 +01:00
Darren Griffin
e8e65a4293 Fix default time_format option. Fixes #24798 (#24819)
* Fix default time_format option. Fixes #24798

* Update en.json

* Update src/translations/en.json

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-03-28 15:36:29 +01:00
Bram Kragten
724adab2d6 Bumped version to 20250328.0 2025-03-28 15:02:51 +01:00
Bram Kragten
345ad6c9c5 Update voice-assistant-setup-step-local.ts 2025-03-28 15:02:37 +01:00
Bram Kragten
a88d066d7e Update text voice wizard install addons step (#24829) 2025-03-28 15:02:15 +01:00
Paulus Schoutsen
a8e5c8482b Hide backup from default dashboard (#24828) 2025-03-28 15:02:14 +01:00
Paulus Schoutsen
d5ff8ab1e1 Do not play pre-announce sound when testing voice on satellite (#24827) 2025-03-28 15:02:13 +01:00
Bram Kragten
e765cc10fb Fix voice flow (#24825)
* Fix voice flow

* Apply suggestions from code review

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2025-03-28 15:02:12 +01:00
Paul Bottein
916dec101f Add hold and double tap action in the UI for every card that supports it. (#24824)
* Add double tap action to button card UI editor

* Add double tap action to light card UI editor

* Add hold action and double tap action to gauge card UI editor

* Add hold action and double tap action to picture glance card UI editor

* Add hold action and double tap action to picture card UI editor

* Add hold action and double tap action to entity card UI editor

* Add hold action and double tap action to elements
2025-03-28 15:02:11 +01:00
Paul Bottein
909fc119b7 Add scroll restoration when using back navigation in dashboard (#24822)
Add scroll restoration when using back navigation with subviews
2025-03-28 15:02:10 +01:00
puddly
8751dc46f4 Show hardware integrations in the integration list (#24820)
Show hardware integrations in the frontend
2025-03-28 15:02:09 +01:00
Paul Bottein
118c25d25f
Bumped version to 20250327.1 2025-03-27 19:12:22 +01:00
Paul Bottein
ae5427a75e
Fix dashboard strategy (#24808) 2025-03-27 19:12:03 +01:00
Paul Bottein
3b6e267fb5
Fallback to state name when the entry doesn't have name (#24805) 2025-03-27 19:12:02 +01:00
Bram Kragten
1770a51303 Bumped version to 20250327.0 2025-03-27 16:46:17 +01:00
Paul Bottein
534df3d378 Add loading state to area strategy (#24803) 2025-03-27 16:44:15 +01:00
Paul Bottein
23229b3e3b Set the max number of columns to 3 for area dashboard (#24802)
* Set the max number of columns to 4 for area dashboard

* Set it to 3
2025-03-27 16:44:14 +01:00
karwosts
94ee99160b Energy device settings fixes (#24801) 2025-03-27 16:44:13 +01:00
Paul Bottein
b009d71e8f Fix take control of the dashboard (#24800) 2025-03-27 16:44:12 +01:00
Bram Kragten
2ab8209622 Align behavior of template selector with text selector (#24796) 2025-03-27 16:44:11 +01:00
Paul Bottein
ed2940edc3 Revert "Restore scroll position when using back navigation in dashboard" (#24795)
Revert "Restore scroll position when using back navigation in dashboard (#24777)"

This reverts commit 9cfcd21a93dd50d61fb64039ce7bec973c721806.
2025-03-27 16:44:11 +01:00
Paul Bottein
e2b9a06242 Fix more info for disabled entities (#24789) 2025-03-27 16:44:10 +01:00
Paul Bottein
a7acee0438 Remove fixed height in ha tile info (#24787)
Remove unused height in ha tile info
2025-03-27 16:44:09 +01:00
Bram Kragten
1208af510c Fix typo in Arithmetic (#24786)
Fix type in Arithmetic
2025-03-27 16:44:08 +01:00
55 changed files with 942 additions and 407 deletions

View File

@ -309,7 +309,7 @@ export class HcMain extends HassElement {
"../../../../src/panels/lovelace/strategies/get-strategy"
);
const config = await generateLovelaceDashboardStrategy(
rawConfig.strategy,
rawConfig,
this.hass!
);
this._handleNewLovelaceConfig(config);
@ -351,10 +351,7 @@ export class HcMain extends HassElement {
"../../../../src/panels/lovelace/strategies/get-strategy"
);
this._handleNewLovelaceConfig(
await generateLovelaceDashboardStrategy(
DEFAULT_CONFIG.strategy,
this.hass!
)
await generateLovelaceDashboardStrategy(DEFAULT_CONFIG, this.hass!)
);
}

View File

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

View File

@ -6,6 +6,10 @@ import {
differenceInMilliseconds,
differenceInMonths,
endOfMonth,
startOfDay,
endOfDay,
differenceInDays,
addDays,
} from "date-fns";
import { toZonedTime, fromZonedTime } from "date-fns-tz";
import type { HassConfig } from "home-assistant-js-websocket";
@ -100,6 +104,32 @@ export const shiftDateRange = (
locale,
config
);
} else if (
calcDateProperty(
startDate,
(date) => startOfDay(date).getMilliseconds() === date.getMilliseconds(),
locale,
config
) &&
calcDateProperty(
endDate,
(date) => endOfDay(date).getMilliseconds() === date.getMilliseconds(),
locale,
config
)
) {
const difference =
((calcDateDifferenceProperty(
endDate,
startDate,
differenceInDays,
locale,
config
) as number) +
1) *
(forward ? 1 : -1);
start = calcDate(startDate, addDays, locale, config, difference);
end = calcDate(endDate, addDays, locale, config, difference);
} else {
const difference =
((calcDateDifferenceProperty(

View File

@ -33,7 +33,14 @@ export const computeEntityEntryName = (
const device = entry.device_id ? hass.devices[entry.device_id] : undefined;
if (!device) {
return name;
if (name) {
return name;
}
const stateObj = hass.states[entry.entity_id] as HassEntity | undefined;
if (stateObj) {
return computeStateName(stateObj);
}
return undefined;
}
const deviceName = computeDeviceName(device);

View File

@ -1,7 +1,11 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { AreaRegistryEntry } from "../../data/area_registry";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
ExtEntityRegistryEntry,
} from "../../data/entity_registry";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import type { HomeAssistant } from "../../types";
@ -19,6 +23,23 @@ export const getEntityContext = (
| EntityRegistryDisplayEntry
| undefined;
if (!entry) {
return {
device: null,
area: null,
floor: null,
};
}
return getEntityEntryContext(entry, hass);
};
export const getEntityEntryContext = (
entry:
| EntityRegistryDisplayEntry
| EntityRegistryEntry
| ExtEntityRegistryEntry,
hass: HomeAssistant
): EntityContext => {
const deviceId = entry?.device_id;
const device = deviceId ? hass.devices[deviceId] : null;
const areaId = entry?.area_id || device?.area_id;

View File

@ -1,13 +1,15 @@
import "@material/mwc-button";
import { mdiAlertOctagram, mdiCheckBold } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../ha-button";
import "../ha-spinner";
import "../ha-svg-icon";
@customElement("ha-progress-button")
export class HaProgressButton extends LitElement {
@property() public label?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public progress = false;
@ -21,14 +23,16 @@ export class HaProgressButton extends LitElement {
public render(): TemplateResult {
const overlay = this._result || this.progress;
return html`
<mwc-button
?raised=${this.raised}
<ha-button
.raised=${this.raised}
.label=${this.label}
.unelevated=${this.unelevated}
.disabled=${this.disabled || this.progress}
class=${this._result || ""}
>
<slot name="icon" slot="icon"></slot>
<slot></slot>
</mwc-button>
</ha-button>
${!overlay
? nothing
: html`
@ -68,12 +72,12 @@ export class HaProgressButton extends LitElement {
pointer-events: none;
}
mwc-button {
ha-button {
transition: all 1s;
pointer-events: initial;
}
mwc-button.success {
ha-button.success {
--mdc-theme-primary: white;
background-color: var(--success-color);
transition: none;
@ -81,13 +85,13 @@ export class HaProgressButton extends LitElement {
pointer-events: none;
}
mwc-button[unelevated].success,
mwc-button[raised].success {
ha-button[unelevated].success,
ha-button[raised].success {
--mdc-theme-primary: var(--success-color);
--mdc-theme-on-primary: white;
}
mwc-button.error {
ha-button.error {
--mdc-theme-primary: white;
background-color: var(--error-color);
transition: none;
@ -95,8 +99,8 @@ export class HaProgressButton extends LitElement {
pointer-events: none;
}
mwc-button[unelevated].error,
mwc-button[raised].error {
ha-button[unelevated].error,
ha-button[raised].error {
--mdc-theme-primary: var(--error-color);
--mdc-theme-on-primary: white;
}
@ -113,8 +117,8 @@ export class HaProgressButton extends LitElement {
color: white;
}
mwc-button.success slot,
mwc-button.error slot {
ha-button.success slot,
ha-button.error slot {
visibility: hidden;
}
:host([destructive]) {

View File

@ -296,7 +296,11 @@ export class StatisticsChart extends LitElement {
align: "left",
},
position: computeRTL(this.hass) ? "right" : "left",
scale: true,
scale:
this.chartType !== "bar" ||
this.logarithmicScale ||
minYAxis !== undefined ||
maxYAxis !== undefined,
min: this._clampYAxis(minYAxis),
max: this._clampYAxis(maxYAxis),
splitLine: {

View File

@ -211,36 +211,12 @@ export class HaRelatedItems extends LitElement {
)}
</mwc-list>`
: nothing}
${this._related.device
? html`<h3>
${this.hass.localize("ui.components.related-items.device")}
</h3>
${this._related.device.map((relatedDeviceId) => {
const device = this.hass.devices[relatedDeviceId];
if (!device) {
return nothing;
}
return html`
<a href="/config/devices/device/${relatedDeviceId}">
<ha-list-item hasMeta graphic="icon">
<ha-svg-icon
.path=${mdiDevices}
slot="graphic"
></ha-svg-icon>
${device.name_by_user || device.name}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>
`;
})} </mwc-list>
`
: nothing}
${this._related.area
? html`<h3>
${this.hass.localize("ui.components.related-items.area")}
</h3>
<mwc-list
>${this._related.area.map((relatedAreaId) => {
<mwc-list>
${this._related.area.map((relatedAreaId) => {
const area = this.hass.areas[relatedAreaId];
if (!area) {
return nothing;
@ -268,8 +244,33 @@ export class HaRelatedItems extends LitElement {
</ha-list-item>
</a>
`;
})}</mwc-list
>`
})}
</mwc-list>`
: nothing}
${this._related.device
? html`<h3>
${this.hass.localize("ui.components.related-items.device")}
</h3>
<mwc-list>
${this._related.device.map((relatedDeviceId) => {
const device = this.hass.devices[relatedDeviceId];
if (!device) {
return nothing;
}
return html`
<a href="/config/devices/device/${relatedDeviceId}">
<ha-list-item hasMeta graphic="icon">
<ha-svg-icon
.path=${mdiDevices}
slot="graphic"
></ha-svg-icon>
${device.name_by_user || device.name}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>
`;
})}
</mwc-list>`
: nothing}
${this._related.entity
? html`

View File

@ -69,11 +69,14 @@ export class HaTemplateSelector extends LitElement {
}
private _handleChange(ev) {
const value = ev.target.value;
let value = ev.target.value;
if (this.value === value) {
return;
}
this.warn = WARNING_STRINGS.find((str) => value.includes(str));
if (value === "" && !this.required) {
value = undefined;
}
fireEvent(this, "value-changed", { value });
}

View File

@ -26,7 +26,6 @@ export class HaTileInfo extends LitElement {
flex-direction: column;
align-items: flex-start;
justify-content: center;
height: 36px;
}
span {
text-overflow: ellipsis;

View File

@ -49,9 +49,12 @@ export const testAssistSatelliteConnection = (
export const assistSatelliteAnnounce = (
hass: HomeAssistant,
entity_id: string,
message: string
) =>
hass.callService("assist_satellite", "announce", { message }, { entity_id });
args: {
message?: string;
media_id?: string;
preannounce_media_id?: string | null;
}
) => hass.callService("assist_satellite", "announce", args, { entity_id });
export const fetchAssistSatelliteConfiguration = (
hass: HomeAssistant,

View File

@ -38,7 +38,7 @@ export interface Statistic {
export enum StatisticMeanType {
NONE = 0,
ARIMETHIC = 1,
ARITHMETIC = 1,
CIRCULAR = 2,
}

View File

@ -21,8 +21,10 @@ import { stopPropagation } from "../../common/dom/stop_propagation";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { getEntityContext } from "../../common/entity/get_entity_context";
import {
computeEntityEntryName,
computeEntityName,
} from "../../common/entity/compute_entity_name";
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
import { navigate } from "../../common/navigate";
import "../../components/ha-button-menu";
@ -56,6 +58,10 @@ import "./ha-more-info-history-and-logbook";
import "./ha-more-info-info";
import "./ha-more-info-settings";
import "./more-info-content";
import {
getEntityContext,
getEntityEntryContext,
} from "../../common/entity/get_entity_context";
export interface MoreInfoDialogParams {
entityId: string | null;
@ -270,6 +276,11 @@ export class MoreInfoDialog extends LitElement {
this._setView("related");
}
private _breadcrumbClick(ev: Event) {
ev.stopPropagation();
this._setView("related");
}
private async _loadNumericDeviceClasses() {
const deviceClasses = await getSensorNumericDeviceClasses(this.hass);
this._sensorNumericDeviceClasses = deviceClasses.numeric_device_classes;
@ -293,11 +304,18 @@ export class MoreInfoDialog extends LitElement {
this._initialView !== DEFAULT_VIEW && !this._childView;
const showCloseIcon = isDefaultView || isSpecificInitialView;
const context = stateObj ? getEntityContext(stateObj, this.hass) : null;
const context = stateObj
? getEntityContext(stateObj, this.hass)
: this._entry
? getEntityEntryContext(this._entry, this.hass)
: undefined;
const entityName = stateObj
? computeEntityName(stateObj, this.hass)
: undefined;
: this._entry
? computeEntityEntryName(this._entry, this.hass)
: entityId;
const deviceName = context?.device
? computeDeviceName(context.device)
: undefined;
@ -306,7 +324,7 @@ export class MoreInfoDialog extends LitElement {
const breadcrumb = [areaName, deviceName, entityName].filter(
(v): v is string => Boolean(v)
);
const title = this._childView?.viewTitle || breadcrumb.pop();
const title = this._childView?.viewTitle || breadcrumb.pop() || entityId;
return html`
<ha-dialog
@ -337,18 +355,23 @@ export class MoreInfoDialog extends LitElement {
)}
></ha-icon-button-prev>
`}
<span
slot="title"
.title=${title}
@click=${this._enlarge}
class="title"
>
<span slot="title" @click=${this._enlarge} class="title">
${breadcrumb.length > 0
? html`
<p class="breadcrumb">
${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)}
</p>
`
? !__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>
</span>
@ -643,6 +666,7 @@ export class MoreInfoDialog extends LitElement {
.title {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.title p {
@ -663,11 +687,25 @@ export class MoreInfoDialog extends LitElement {
color: var(--secondary-text-color);
font-size: 14px;
line-height: 16px;
margin-top: -6px;
--mdc-icon-size: 16px;
padding: 4px;
margin: -4px;
margin-top: -10px;
background: none;
border: none;
outline: none;
display: inline-flex;
border-radius: 6px;
transition: background-color 180ms ease-in-out;
}
.title .breadcrumb {
--mdc-icon-size: 16px;
.title button.breadcrumb {
cursor: pointer;
}
.title button.breadcrumb:focus-visible,
.title button.breadcrumb:hover {
background-color: rgba(var(--rgb-secondary-text-color), 0.08);
}
`,
];

View File

@ -3,7 +3,6 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { storage } from "../../common/decorators/storage";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button";
import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-textarea";
import type { HaTextArea } from "../../components/ha-textarea";
@ -11,7 +10,7 @@ import { convertTextToSpeech } from "../../data/tts";
import type { HomeAssistant } from "../../types";
import { showAlertDialog } from "../generic/show-dialog-box";
import type { TTSTryDialogParams } from "./show-dialog-tts-try";
import "../../components/ha-spinner";
import "../../components/buttons/ha-progress-button";
@customElement("dialog-tts-try")
export class TTSTryDialog extends LitElement {
@ -81,28 +80,17 @@ export class TTSTryDialog extends LitElement {
?dialogInitialFocus=${!this._defaultMessage}
>
</ha-textarea>
${this._loadingExample
? html`
<ha-spinner
size="small"
slot="primaryAction"
class="loading"
></ha-spinner>
`
: html`
<ha-button
?dialogInitialFocus=${Boolean(this._defaultMessage)}
slot="primaryAction"
.label=${this.hass.localize("ui.dialogs.tts-try.play")}
@click=${this._playExample}
.disabled=${!this._valid}
>
<ha-svg-icon
slot="icon"
.path=${mdiPlayCircleOutline}
></ha-svg-icon>
</ha-button>
`}
<ha-progress-button
.progress=${this._loadingExample}
?dialogInitialFocus=${Boolean(this._defaultMessage)}
slot="primaryAction"
.label=${this.hass.localize("ui.dialogs.tts-try.play")}
@click=${this._playExample}
.disabled=${!this._valid}
>
<ha-svg-icon slot="icon" .path=${mdiPlayCircleOutline}></ha-svg-icon>
</ha-progress-button>
</ha-dialog>
`;
}

View File

@ -243,7 +243,7 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
private readonly _ttsHostName = "core-piper";
private readonly _ttsPort = "10200";
private readonly _ttsPort = 10200;
private get _sttProviderName() {
return this.localOption === "focused_local"
@ -263,7 +263,7 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
: "core-whisper";
}
private readonly _sttPort = "10300";
private readonly _sttPort = 10300;
private async _findLocalEntities() {
const wyomingEntities = Object.values(this.hass.entities).filter(
@ -325,14 +325,16 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
(flow) =>
flow.handler === "wyoming" &&
flow.context.source === "hassio" &&
(flow.context.configuration_url.includes(
type === "tts" ? this._ttsHostName : this._sttHostName
) ||
flow.context.title_placeholders.title
.toLowerCase()
.includes(
type === "tts" ? this._ttsProviderName : this._sttProviderName
))
((flow.context.configuration_url &&
flow.context.configuration_url.includes(
type === "tts" ? this._ttsAddonName : this._sttAddonName
)) ||
(flow.context.title_placeholders.name &&
flow.context.title_placeholders.name
.toLowerCase()
.includes(
type === "tts" ? this._ttsProviderName : this._sttProviderName
)))
);
}
@ -357,40 +359,24 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
}
const pipelines = await listAssistPipelines(this.hass);
const preferredPipeline = pipelines.pipelines.find(
(pipeline) => pipeline.id === pipelines.preferred_pipeline
);
if (pipelines.preferred_pipeline) {
pipelines.pipelines.sort((a) =>
a.id === pipelines.preferred_pipeline ? -1 : 0
);
}
const ttsEntityIds = this._localTts.map((ent) => ent.entity_id);
const sttEntityIds = this._localStt.map((ent) => ent.entity_id);
if (preferredPipeline) {
if (
preferredPipeline.conversation_engine ===
"conversation.home_assistant" &&
preferredPipeline.tts_engine &&
ttsEntityIds.includes(preferredPipeline.tts_engine) &&
preferredPipeline.stt_engine &&
sttEntityIds.includes(preferredPipeline.stt_engine)
) {
await this.hass.callService(
"select",
"select_option",
{ option: "preferred" },
{ entity_id: this.assistConfiguration?.pipeline_entity_id }
);
this._nextStep();
return;
}
}
let localPipeline = pipelines.pipelines.find(
(pipeline) =>
pipeline.conversation_engine === "conversation.home_assistant" &&
pipeline.tts_engine &&
ttsEntityIds.includes(pipeline.tts_engine) &&
pipeline.stt_engine &&
sttEntityIds.includes(pipeline.stt_engine)
sttEntityIds.includes(pipeline.stt_engine) &&
pipeline.language.split("-")[0] === this.language.split("-")[0]
);
if (!localPipeline) {
@ -463,7 +449,7 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
}
let pipelineName = this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.local.local_pipeline"
`ui.panel.config.voice_assistants.satellite_wizard.local.${this.localOption}_pipeline`
);
let i = 1;
while (
@ -472,7 +458,7 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
(pipeline) => pipeline.name === pipelineName
)
) {
pipelineName = `${this.hass.localize("ui.panel.config.voice_assistants.satellite_wizard.local.local_pipeline")} ${i}`;
pipelineName = `${this.hass.localize(`ui.panel.config.voice_assistants.satellite_wizard.local.${this.localOption}_pipeline`)} ${i}`;
i++;
}

View File

@ -15,7 +15,7 @@ import {
} from "../../data/assist_pipeline";
import type { AssistSatelliteConfiguration } from "../../data/assist_satellite";
import { fetchCloudStatus } from "../../data/cloud";
import type { LanguageScores } from "../../data/conversation";
import type { LanguageScore, LanguageScores } from "../../data/conversation";
import { getLanguageScores, listAgents } from "../../data/conversation";
import { listSTTEngines } from "../../data/stt";
import { listTTSEngines, listTTSVoices } from "../../data/tts";
@ -26,6 +26,12 @@ import { documentationUrl } from "../../util/documentation-url";
const OPTIONS = ["cloud", "focused_local", "full_local"] as const;
const EMPTY_SCORE: LanguageScore = {
cloud: 0,
focused_local: 0,
full_local: 0,
};
@customElement("ha-voice-assistant-setup-step-pipeline")
export class HaVoiceAssistantSetupStepPipeline extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -61,12 +67,12 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
this._languageScores
) {
const lang = this.language;
if (this._value && this._languageScores[lang][this._value] === 0) {
if (this._value && this._languageScores[lang]?.[this._value] === 0) {
this._value = undefined;
}
if (!this._value) {
this._value = this._getOptions(
this._languageScores[lang],
this._languageScores[lang] || EMPTY_SCORE,
this.hass.localize
).supportedOptions[0]?.value as
| "cloud"
@ -147,12 +153,9 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
</div>`;
}
const score = this._languageScores[this.language];
const score = this._languageScores[this.language] || EMPTY_SCORE;
const options = this._getOptions(
score || { cloud: 3, focused_local: 0, full_local: 0 },
this.hass.localize
);
const options = this._getOptions(score, this.hass.localize);
const performance = !this._value
? ""
@ -162,11 +165,11 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
const commands = !this._value
? ""
: score?.[this._value] > 2
: score[this._value] > 2
? "high"
: score?.[this._value] > 1
: score[this._value] > 1
? "ready"
: score?.[this._value] > 0
: score[this._value] > 0
? "low"
: "";
@ -243,7 +246,7 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
private async _fetchData() {
const cloud =
(await this._hasCloud()) && (await this._createCloudPipeline());
(await this._hasCloud()) && (await this._createCloudPipeline(false));
if (!cloud) {
this._cloudChecked = true;
this._languageScores = (await getLanguageScores(this.hass)).languages;
@ -261,7 +264,7 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
return true;
}
private async _createCloudPipeline(): Promise<boolean> {
private async _createCloudPipeline(useLanguage: boolean): Promise<boolean> {
let cloudTtsEntityId;
let cloudSttEntityId;
for (const entity of Object.values(this.hass.entities)) {
@ -281,36 +284,20 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
}
try {
const pipelines = await listAssistPipelines(this.hass);
const preferredPipeline = pipelines.pipelines.find(
(pipeline) => pipeline.id === pipelines.preferred_pipeline
);
if (preferredPipeline) {
if (
preferredPipeline.conversation_engine ===
"conversation.home_assistant" &&
preferredPipeline.tts_engine === cloudTtsEntityId &&
preferredPipeline.stt_engine === cloudSttEntityId
) {
await this.hass.callService(
"select",
"select_option",
{ option: "preferred" },
{ entity_id: this.assistConfiguration?.pipeline_entity_id }
);
fireEvent(this, "next-step", {
step: STEP.SUCCESS,
noPrevious: true,
});
return true;
}
if (pipelines.preferred_pipeline) {
pipelines.pipelines.sort((a) =>
a.id === pipelines.preferred_pipeline ? -1 : 0
);
}
let cloudPipeline = pipelines.pipelines.find(
(pipeline) =>
pipeline.conversation_engine === "conversation.home_assistant" &&
pipeline.tts_engine === cloudTtsEntityId &&
pipeline.stt_engine === cloudSttEntityId
pipeline.stt_engine === cloudSttEntityId &&
(!useLanguage ||
pipeline.language.split("-")[0] === this.language!.split("-")[0])
);
if (!cloudPipeline) {
@ -402,7 +389,7 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
private async _setupCloud() {
if (await this._hasCloud()) {
this._createCloudPipeline();
this._createCloudPipeline(true);
return;
}
fireEvent(this, "next-step", { step: STEP.CLOUD });

View File

@ -246,7 +246,10 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
if (!this.assistEntityId) {
return;
}
await assistSatelliteAnnounce(this.hass, this.assistEntityId, message);
await assistSatelliteAnnounce(this.hass, this.assistEntityId, {
message,
preannounce_media_id: null,
});
}
private _testWakeWord() {

View File

@ -152,12 +152,14 @@ export class EnergyDeviceSettings extends LitElement {
device_consumptions: this.preferences
.device_consumption as DeviceConsumptionEnergyPreference[],
saveCallback: async (newDevice) => {
await this._savePreferences({
const newPrefs = {
...this.preferences,
device_consumption: this.preferences.device_consumption.map((d) =>
d === origDevice ? newDevice : d
),
});
};
this._sanitizeParents(newPrefs);
await this._savePreferences(newPrefs);
},
});
}
@ -177,6 +179,15 @@ export class EnergyDeviceSettings extends LitElement {
});
}
private _sanitizeParents(prefs: EnergyPreferences) {
const statIds = prefs.device_consumption.map((d) => d.stat_consumption);
prefs.device_consumption.forEach((d) => {
if (d.included_in_stat && !statIds.includes(d.included_in_stat)) {
delete d.included_in_stat;
}
});
}
private async _deleteDevice(ev) {
const deviceToDelete: DeviceConsumptionEnergyPreference =
ev.currentTarget.device;
@ -196,14 +207,7 @@ export class EnergyDeviceSettings extends LitElement {
(device) => device !== deviceToDelete
),
};
newPrefs.device_consumption.forEach((d, idx) => {
if (d.included_in_stat === deviceToDelete.stat_consumption) {
newPrefs.device_consumption[idx] = {
...newPrefs.device_consumption[idx],
};
delete newPrefs.device_consumption[idx].included_in_stat;
}
});
this._sanitizeParents(newPrefs);
await this._savePreferences(newPrefs);
} catch (err: any) {
showAlertDialog(this, { title: `Failed to save config: ${err.message}` });

View File

@ -74,6 +74,7 @@ export class DialogEnergyDeviceSettings
this._possibleParents = this._params.device_consumptions.filter(
(d) =>
d.stat_consumption !== this._device!.stat_consumption &&
d.stat_consumption !== this._params?.device?.stat_consumption &&
!children.includes(d.stat_consumption)
);
}
@ -160,18 +161,26 @@ export class DialogEnergyDeviceSettings
naturalMenuWidth
clearable
>
${this._possibleParents.map(
(stat) => html`
<mwc-list-item .value=${stat.stat_consumption}
>${stat.name ||
getStatisticLabel(
this.hass,
stat.stat_consumption,
this._params?.statsMetadata?.[stat.stat_consumption]
)}</mwc-list-item
>
`
)}
${!this._possibleParents.length
? html`
<mwc-list-item disabled value="-"
>${this.hass.localize(
"ui.panel.config.energy.device_consumption.dialog.no_upstream_devices"
)}</mwc-list-item
>
`
: this._possibleParents.map(
(stat) => html`
<mwc-list-item .value=${stat.stat_consumption}
>${stat.name ||
getStatisticLabel(
this.hass,
stat.stat_consumption,
this._params?.statsMetadata?.[stat.stat_consumption]
)}</mwc-list-item
>
`
)}
</ha-select>
<mwc-button @click=${this.closeDialog} slot="secondaryAction">

View File

@ -120,7 +120,7 @@ class HaConfigIntegrations extends SubscribeMixin(HassRouterPage) {
const existingEntries = fullUpdate ? [] : this._configEntries;
this._configEntries = [...existingEntries!, ...newEntries];
},
{ type: ["device", "hub", "service"] }
{ type: ["device", "hub", "service", "hardware"] }
),
subscribeConfigFlowInProgress(this.hass, async (flowsInProgress) => {
const integrations = new Set<string>();

View File

@ -11,12 +11,19 @@ export class DialogZWaveJSAddNode extends HTMLElement {
public configEntryId!: string;
connectedCallback() {
this._openDialog();
}
private async _openDialog() {
await navigate(
`/config/devices/dashboard?config_entry=${this.configEntryId}`,
{
replace: true,
}
);
showZWaveJSAddNodeDialog(this, {
entry_id: this.configEntryId,
});
navigate(`/config/devices/dashboard?config_entry=${this.configEntryId}`, {
replace: true,
});
}
}

View File

@ -78,9 +78,6 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
return {
type: "button",
tap_action: {
action: "toggle",
},
entity: foundEntities[0] || "",
};
}
@ -164,6 +161,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
action: getEntityDefaultButtonAction(config.entity),
},
hold_action: { action: "more-info" },
double_tap_action: { action: "none" },
show_icon: true,
show_name: true,
state_color: true,

View File

@ -65,7 +65,9 @@ export class HuiClockCard extends LitElement implements LovelaceCard {
minute: "2-digit",
second: "2-digit",
hourCycle: useAmPm(locale) ? "h12" : "h23",
timeZone: resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
timeZone:
this._config?.time_zone ||
resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
});
this._tick();
@ -79,7 +81,7 @@ export class HuiClockCard extends LitElement implements LovelaceCard {
public getGridOptions(): LovelaceGridOptions {
if (this._config?.clock_size === "medium") {
return {
min_rows: 1,
min_rows: this._config?.title ? 2 : 1,
rows: 2,
max_rows: 4,
min_columns: 4,
@ -101,7 +103,7 @@ export class HuiClockCard extends LitElement implements LovelaceCard {
min_rows: 1,
rows: 1,
max_rows: 4,
min_columns: 4,
min_columns: 3,
columns: 6,
};
}
@ -160,6 +162,9 @@ export class HuiClockCard extends LitElement implements LovelaceCard {
? `size-${this._config.clock_size}`
: ""}"
>
${this._config.title !== undefined
? html`<div class="time-title">${this._config.title}</div>`
: nothing}
<div class="time-parts">
<div class="time-part hour">${this._timeHour}</div>
<div class="time-part minute">${this._timeMinute}</div>
@ -182,9 +187,41 @@ export class HuiClockCard extends LitElement implements LovelaceCard {
.time-wrapper {
display: flex;
height: 100%;
height: calc(100% - 12px);
align-items: center;
flex-direction: column;
justify-content: center;
padding: 6px 8px;
row-gap: 6px;
}
.time-wrapper.size-medium,
.time-wrapper.size-large {
height: calc(100% - 32px);
padding: 16px;
row-gap: 12px;
}
.time-title {
color: var(--primary-text-color);
font-size: 14px;
font-weight: 400;
line-height: 18px;
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
.time-wrapper.size-medium .time-title {
font-size: 18px;
line-height: 21px;
}
.time-wrapper.size-large .time-title {
font-size: 24px;
line-height: 28px;
}
.time-parts {
@ -197,7 +234,11 @@ export class HuiClockCard extends LitElement implements LovelaceCard {
font-size: 2rem;
font-weight: 500;
line-height: 0.8;
padding: 16px 0;
direction: ltr;
}
.time-title + .time-parts {
font-size: 1.5rem;
}
.time-wrapper.size-medium .time-parts {
@ -242,8 +283,7 @@ export class HuiClockCard extends LitElement implements LovelaceCard {
.time-parts .time-part.second,
.time-parts .time-part.am-pm {
font-size: 12px;
font-weight: 500;
font-size: 10px;
margin-left: 4px;
}

View File

@ -46,7 +46,10 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
throw new Error("Image required");
}
this._config = config;
this._config = {
tap_action: { action: "more-info" },
...config,
};
}
protected shouldUpdate(changedProps: PropertyValues): boolean {

View File

@ -6,9 +6,11 @@ import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_elemen
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/ha-card";
import type { CameraEntity } from "../../../data/camera";
import type { ImageEntity } from "../../../data/image";
import { computeImageUrl } from "../../../data/image";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import type { PersonEntity } from "../../../data/person";
import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { findEntities } from "../common/find-entities";
@ -19,8 +21,6 @@ import "../components/hui-image";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
import type { PictureEntityCardConfig } from "./types";
import type { CameraEntity } from "../../../data/camera";
import type { PersonEntity } from "../../../data/person";
export const STUB_IMAGE =
"https://demo.home-assistant.io/stub_config/bedroom.png";
@ -75,7 +75,12 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
throw new Error("No image source configured");
}
this._config = { show_name: true, show_state: true, ...config };
this._config = {
show_name: true,
show_state: true,
tap_action: { action: "more-info" },
...config,
};
}
protected shouldUpdate(changedProps: PropertyValues): boolean {

View File

@ -105,7 +105,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
});
this._config = {
hold_action: { action: "more-info" },
tap_action: { action: "more-info" },
...config,
};
}

View File

@ -349,9 +349,11 @@ export interface MarkdownCardConfig extends LovelaceCardConfig {
export interface ClockCardConfig extends LovelaceCardConfig {
type: "clock";
title?: string;
clock_size?: "small" | "medium" | "large";
show_seconds?: boolean | undefined;
time_format?: TimeFormat;
time_zone?: string;
}
export interface MediaControlCardConfig extends LovelaceCardConfig {

View File

@ -50,7 +50,7 @@ const HIDE_DOMAIN = new Set([
...ASSIST_ENTITIES,
]);
const HIDE_PLATFORM = new Set(["mobile_app"]);
const HIDE_PLATFORM = new Set(["backup", "mobile_app"]);
interface SplittedByAreaDevice {
areasWithEntities: Record<string, HassEntity[]>;

View File

@ -1,12 +1,13 @@
import { mdiGestureTap } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { any, assert, literal, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-form/ha-form";
import type { LovelacePictureElementEditor } from "../../../types";
import type { IconElementConfig } from "../../../elements/types";
import type { LovelacePictureElementEditor } from "../../../types";
import { actionConfigStruct } from "../../structs/action-struct";
const iconElementConfigStruct = object({
@ -25,16 +26,35 @@ const SCHEMA = [
{ name: "title", selector: { text: {} } },
{ name: "entity", selector: { entity: {} } },
{
name: "tap_action",
selector: {
ui_action: {},
},
},
{
name: "hold_action",
selector: {
ui_action: {},
},
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
{ name: "style", selector: { object: {} } },
] as const;

View File

@ -1,12 +1,13 @@
import { mdiGestureTap } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { any, assert, literal, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-form/ha-form";
import type { LovelacePictureElementEditor } from "../../../types";
import type { ImageElementConfig } from "../../../elements/types";
import type { LovelacePictureElementEditor } from "../../../types";
import { actionConfigStruct } from "../../structs/action-struct";
const imageElementConfigStruct = object({
@ -30,16 +31,35 @@ const SCHEMA = [
{ name: "entity", selector: { entity: {} } },
{ name: "title", selector: { text: {} } },
{
name: "tap_action",
selector: {
ui_action: {},
},
},
{
name: "hold_action",
selector: {
ui_action: {},
},
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
{ name: "image", selector: { image: {} } },
{ name: "camera_image", selector: { entity: { domain: "camera" } } },

View File

@ -1,6 +1,7 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { any, assert, literal, object, optional, string } from "superstruct";
import { mdiGestureTap } from "@mdi/js";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
@ -23,16 +24,35 @@ const SCHEMA = [
{ name: "entity", required: true, selector: { entity: {} } },
{ name: "title", selector: { text: {} } },
{
name: "tap_action",
selector: {
ui_action: {},
},
},
{
name: "hold_action",
selector: {
ui_action: {},
},
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
{ name: "style", selector: { object: {} } },
] as const;

View File

@ -1,3 +1,4 @@
import { mdiGestureTap } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
@ -10,11 +11,11 @@ import {
string,
} from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-form/ha-form";
import type { LovelacePictureElementEditor } from "../../../types";
import type { StateIconElementConfig } from "../../../elements/types";
import type { LovelacePictureElementEditor } from "../../../types";
import { actionConfigStruct } from "../../structs/action-struct";
const stateIconElementConfigStruct = object({
@ -35,16 +36,35 @@ const SCHEMA = [
{ name: "title", selector: { text: {} } },
{ name: "state_color", default: true, selector: { boolean: {} } },
{
name: "tap_action",
selector: {
ui_action: {},
},
},
{
name: "hold_action",
selector: {
ui_action: {},
},
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
{ name: "style", selector: { object: {} } },
] as const;

View File

@ -1,12 +1,13 @@
import { mdiGestureTap } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { any, assert, literal, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-form/ha-form";
import type { LovelacePictureElementEditor } from "../../../types";
import type { StateLabelElementConfig } from "../../../elements/types";
import type { LovelacePictureElementEditor } from "../../../types";
import { actionConfigStruct } from "../../structs/action-struct";
const stateLabelElementConfigStruct = object({
@ -35,16 +36,35 @@ const SCHEMA = [
{ name: "suffix", selector: { text: {} } },
{ name: "title", selector: { text: {} } },
{
name: "tap_action",
selector: {
ui_action: {},
},
},
{
name: "hold_action",
selector: {
ui_action: {},
},
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
{ name: "style", selector: { object: {} } },
] as const;

View File

@ -1,3 +1,4 @@
import { mdiGestureTap } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@ -28,6 +29,7 @@ const cardConfigStruct = assign(
icon_height: optional(string()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
theme: optional(string()),
show_state: optional(boolean()),
})
@ -86,20 +88,43 @@ export class HuiButtonCardEditor
],
},
{
name: "tap_action",
selector: {
ui_action: {
default_action: getEntityDefaultButtonAction(entityId),
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: getEntityDefaultButtonAction(entityId),
},
},
},
},
},
{
name: "hold_action",
selector: {
ui_action: {
default_action: "more-info",
{
name: "hold_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: [
{
name: "double_tap_action",
selector: {
ui_action: {
default_action: "none",
},
},
},
],
},
],
},
] as const satisfies readonly HaFormSchema[]
);

View File

@ -1,3 +1,4 @@
import timezones from "google-timezones-json";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@ -9,6 +10,7 @@ import {
literal,
object,
optional,
string,
union,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
@ -27,10 +29,12 @@ import { TimeFormat } from "../../../../data/translation";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
title: optional(string()),
clock_size: optional(
union([literal("small"), literal("medium"), literal("large")])
),
time_format: optional(enums(Object.values(TimeFormat))),
time_zone: optional(enums(Object.keys(timezones))),
show_seconds: optional(boolean()),
})
);
@ -47,6 +51,7 @@ export class HuiClockCardEditor
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
{ name: "title", selector: { text: {} } },
{
name: "clock_size",
selector: {
@ -61,18 +66,13 @@ export class HuiClockCardEditor
},
},
},
{
name: "show_seconds",
selector: {
boolean: {},
},
},
{ name: "show_seconds", selector: { boolean: {} } },
{
name: "time_format",
selector: {
select: {
mode: "dropdown",
options: Object.values(TimeFormat).map((value) => ({
options: ["auto", ...Object.values(TimeFormat)].map((value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.clock.time_formats.${value}`
@ -81,12 +81,33 @@ export class HuiClockCardEditor
},
},
},
{
name: "time_zone",
selector: {
select: {
mode: "dropdown",
options: [
[
"auto",
localize(
`ui.panel.lovelace.editor.card.clock.time_zones.auto`
),
],
...Object.entries(timezones as Record<string, string>),
].map(([key, value]) => ({
value: key,
label: value,
})),
},
},
},
] as const satisfies readonly HaFormSchema[]
);
private _data = memoizeOne((config) => ({
clock_size: "small",
time_format: TimeFormat.language,
time_zone: "auto",
time_format: "auto",
show_seconds: false,
...config,
}));
@ -113,6 +134,13 @@ export class HuiClockCardEditor
}
private _valueChanged(ev: CustomEvent): void {
if (ev.detail.value.time_zone === "auto") {
delete ev.detail.value.time_zone;
}
if (ev.detail.value.time_format === "auto") {
delete ev.detail.value.time_format;
}
fireEvent(this, "config-changed", { config: ev.detail.value });
}
@ -120,6 +148,10 @@ export class HuiClockCardEditor
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "title":
return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.title"
);
case "clock_size":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.clock_size`
@ -128,6 +160,10 @@ export class HuiClockCardEditor
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.time_format`
);
case "time_zone":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.time_zone`
);
case "show_seconds":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.show_seconds`

View File

@ -1,3 +1,4 @@
import { mdiGestureTap } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@ -16,14 +17,21 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import { DEFAULT_MAX, DEFAULT_MIN } from "../../cards/hui-gauge-card";
import type { GaugeCardConfig } from "../../cards/types";
import type { UiAction } from "../../components/hui-action-editor";
import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { DEFAULT_MIN, DEFAULT_MAX } from "../../cards/hui-gauge-card";
import type { UiAction } from "../../components/hui-action-editor";
const TAP_ACTIONS: UiAction[] = ["navigate", "url", "perform-action", "none"];
const TAP_ACTIONS: UiAction[] = [
"more-info",
"navigate",
"url",
"perform-action",
"assist",
"none",
];
const gaugeSegmentStruct = object({
from: number(),
@ -134,13 +142,37 @@ export class HuiGaugeCardEditor
] as const)
: []),
{
name: "tap_action",
selector: {
ui_action: {
actions: TAP_ACTIONS,
default_action: "more-info",
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
actions: TAP_ACTIONS,
default_action: "more-info",
},
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
actions: TAP_ACTIONS,
default_action: "none" as const,
},
},
})
),
},
],
},
] as const
);
@ -231,7 +263,13 @@ export class HuiGaugeCardEditor
return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.unit"
);
case "interactions":
return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.interactions"
);
case "tap_action":
case "hold_action":
case "double_tap_action":
return `${this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
)} (${this.hass!.localize(

View File

@ -2,6 +2,7 @@ import type { CSSResultGroup } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
import { mdiGestureTap } from "@mdi/js";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
@ -19,6 +20,7 @@ const cardConfigStruct = assign(
entity: optional(string()),
theme: optional(string()),
icon: optional(string()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
})
@ -48,12 +50,43 @@ const SCHEMA = [
},
{ name: "theme", selector: { theme: {} } },
{
name: "hold_action",
selector: { ui_action: {} },
},
{
name: "double_tap_action",
selector: { ui_action: {} },
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "toggle",
},
},
},
{
name: "hold_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: [
{
name: "double_tap_action",
selector: {
ui_action: {
default_action: "none",
},
},
},
],
},
],
},
] as const;

View File

@ -1,3 +1,4 @@
import { mdiGestureTap } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
@ -18,6 +19,7 @@ const cardConfigStruct = assign(
image_entity: optional(string()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
theme: optional(string()),
alt_text: optional(string()),
})
@ -32,12 +34,35 @@ const SCHEMA = [
{ name: "alt_text", selector: { text: {} } },
{ name: "theme", selector: { theme: {} } },
{
name: "tap_action",
selector: { ui_action: {} },
},
{
name: "hold_action",
selector: { ui_action: {} },
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
] as const;

View File

@ -1,18 +1,19 @@
import { mdiGestureTap } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, boolean, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import { STUB_IMAGE } from "../../cards/hui-picture-entity-card";
import type { PictureEntityCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { configElementStyle } from "./config-elements-style";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { STUB_IMAGE } from "../../cards/hui-picture-entity-card";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@ -25,6 +26,7 @@ const cardConfigStruct = assign(
aspect_ratio: optional(string()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
show_name: optional(boolean()),
show_state: optional(boolean()),
theme: optional(string()),
@ -64,12 +66,35 @@ const SCHEMA = [
},
{ name: "theme", selector: { theme: {} } },
{
name: "tap_action",
selector: { ui_action: {} },
},
{
name: "hold_action",
selector: { ui_action: {} },
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
] as const;
@ -132,6 +157,7 @@ export class HuiPictureEntityCardEditor
case "theme":
case "tap_action":
case "hold_action":
case "double_tap_action":
return `${this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
)} (${this.hass!.localize(

View File

@ -2,6 +2,7 @@ import type { CSSResultGroup } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { array, assert, assign, object, optional, string } from "superstruct";
import { mdiGestureTap } from "@mdi/js";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
@ -29,6 +30,7 @@ const cardConfigStruct = assign(
aspect_ratio: optional(string()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
entities: array(entitiesConfigStruct),
theme: optional(string()),
})
@ -56,12 +58,35 @@ const SCHEMA = [
{ name: "entity", selector: { entity: {} } },
{ name: "theme", selector: { theme: {} } },
{
name: "tap_action",
selector: { ui_action: {} },
},
{
name: "hold_action",
selector: { ui_action: {} },
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
] as const;
@ -136,6 +161,7 @@ export class HuiPictureGlanceCardEditor
case "theme":
case "tap_action":
case "hold_action":
case "double_tap_action":
return `${this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
)} (${this.hass!.localize(

View File

@ -1,3 +1,4 @@
import { mdiGestureTap } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@ -5,12 +6,13 @@ import {
assert,
assign,
boolean,
number,
object,
optional,
string,
number,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
@ -22,7 +24,6 @@ import type { WeatherForecastCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { supportsFeature } from "../../../../common/entity/supports-feature";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@ -239,6 +240,37 @@ export class HuiWeatherForecastCardEditor
selector: { number: { min: 1, max: 12 } },
default: 5,
},
{
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
] as const)
: []),
] as const

View File

@ -31,7 +31,7 @@ export class HuiIconElement extends LitElement implements LovelaceElement {
throw Error("Icon required");
}
this._config = { hold_action: { action: "more-info" }, ...config };
this._config = { tap_action: { action: "more-info" }, ...config };
}
protected render() {

View File

@ -29,7 +29,7 @@ export class HuiImageElement extends LitElement implements LovelaceElement {
throw Error("Invalid configuration");
}
this._config = { hold_action: { action: "more-info" }, ...config };
this._config = { tap_action: { action: "more-info" }, ...config };
this.classList.toggle(
"clickable",

View File

@ -60,7 +60,7 @@ export class HuiStateBadgeElement
throw Error("Entity required");
}
this._config = { hold_action: { action: "more-info" }, ...config };
this._config = { tap_action: { action: "more-info" }, ...config };
}
protected shouldUpdate(changedProps: PropertyValues): boolean {

View File

@ -59,7 +59,7 @@ export class HuiStateIconElement extends LitElement implements LovelaceElement {
this._config = {
state_color: true,
hold_action: { action: "more-info" },
tap_action: { action: "more-info" },
...config,
};
}

View File

@ -56,7 +56,7 @@ class HuiStateLabelElement extends LitElement implements LovelaceElement {
throw Error("Entity required");
}
this._config = { hold_action: { action: "more-info" }, ...config };
this._config = { tap_action: { action: "more-info" }, ...config };
}
protected shouldUpdate(changedProps: PropertyValues): boolean {

View File

@ -187,7 +187,7 @@ export class LovelacePanel extends LitElement {
private async _regenerateConfig() {
const conf = await generateLovelaceDashboardStrategy(
DEFAULT_CONFIG.strategy,
DEFAULT_CONFIG,
this.hass!
);
this._setLovelaceConfig(conf, DEFAULT_CONFIG, "generated");
@ -281,10 +281,7 @@ export class LovelacePanel extends LitElement {
// We need these to generate a dashboard, wait for them
return;
}
conf = await generateLovelaceDashboardStrategy(
rawConf.strategy,
this.hass!
);
conf = await generateLovelaceDashboardStrategy(rawConf, this.hass!);
} else {
conf = rawConf;
}
@ -301,7 +298,7 @@ export class LovelacePanel extends LitElement {
return;
}
conf = await generateLovelaceDashboardStrategy(
DEFAULT_CONFIG.strategy,
DEFAULT_CONFIG,
this.hass!
);
rawConf = DEFAULT_CONFIG;
@ -378,10 +375,7 @@ export class LovelacePanel extends LitElement {
let conf: LovelaceConfig;
// If strategy defined, apply it here.
if (isStrategyDashboard(newConfig)) {
conf = await generateLovelaceDashboardStrategy(
newConfig.strategy,
this.hass!
);
conf = await generateLovelaceDashboardStrategy(newConfig, this.hass!);
} else {
conf = newConfig;
}
@ -415,7 +409,7 @@ export class LovelacePanel extends LitElement {
try {
// Optimistic update
const generatedConf = await generateLovelaceDashboardStrategy(
DEFAULT_CONFIG.strategy,
DEFAULT_CONFIG,
this.hass!
);
this._updateLovelace({

View File

@ -76,9 +76,9 @@ import { getLovelaceStrategy } from "./strategies/get-strategy";
import { isLegacyStrategyConfig } from "./strategies/legacy-strategy";
import type { Lovelace } from "./types";
import "./views/hui-view";
import "./views/hui-view-container";
import type { HUIView } from "./views/hui-view";
import "./views/hui-view-background";
import "./views/hui-view-container";
@customElement("hui-root")
class HUIRoot extends LitElement {
@ -101,6 +101,8 @@ class HUIRoot extends LitElement {
private _viewScrollPositions: Record<string, number> = {};
private _restoreScroll = false;
private _debouncedConfigChanged: () => void;
private _conversation = memoizeOne((_components) =>
@ -112,7 +114,7 @@ class HUIRoot extends LitElement {
// The view can trigger a re-render when it knows that certain
// web components have been loaded.
this._debouncedConfigChanged = debounce(
() => this._selectView(this._curView, true, false),
() => this._selectView(this._curView, true),
100,
false
);
@ -487,6 +489,10 @@ class HUIRoot extends LitElement {
this.toggleAttribute("scrolled", window.scrollY !== 0);
};
private _handlePopState = () => {
this._restoreScroll = true;
};
private _isVisible = (view: LovelaceViewConfig) =>
Boolean(
this._editMode ||
@ -528,21 +534,19 @@ class HUIRoot extends LitElement {
passive: true,
});
window.addEventListener("popstate", this._handlePopState);
// Disable history scroll restoration because it is managed manually here
window.history.scrollRestoration = "manual";
}
public disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener("scroll", this._handleWindowScroll);
window.removeEventListener("popstate", this._handlePopState);
this.toggleAttribute("scrolled", window.scrollY !== 0);
// Re-enable history scroll restoration when leaving the page
window.history.scrollRestoration = "auto";
}
private _restoreScroll = false;
private _handlePopState = () => {
// If we navigated back, we want to restore the scroll position.
this._restoreScroll = true;
};
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
@ -622,8 +626,16 @@ class HUIRoot extends LitElement {
}
// Will allow for ripples to start rendering
afterNextRender(() => {
this._selectView(newSelectView, force, this._restoreScroll);
this._restoreScroll = false;
if (changedProperties.has("route")) {
const position =
(this._restoreScroll && this._viewScrollPositions[newSelectView]) ||
0;
this._restoreScroll = false;
requestAnimationFrame(() =>
scrollTo({ behavior: "auto", top: position })
);
}
this._selectView(newSelectView, force);
});
}
}
@ -932,15 +944,12 @@ class HUIRoot extends LitElement {
}
}
private _selectView(
viewIndex: HUIRoot["_curView"],
force: boolean,
restoreScroll: boolean
): void {
private _selectView(viewIndex: HUIRoot["_curView"], force: boolean): void {
if (!force && this._curView === viewIndex) {
return;
}
// Save scroll position of current view
if (this._curView != null) {
this._viewScrollPositions[this._curView] = window.scrollY;
}
@ -983,15 +992,10 @@ class HUIRoot extends LitElement {
if (!force && this._viewCache![viewIndex]) {
view = this._viewCache![viewIndex];
const position = restoreScroll
? this._viewScrollPositions[viewIndex] || 0
: 0;
setTimeout(() => scrollTo({ behavior: "auto", top: position }), 0);
} else {
view = document.createElement("hui-view");
view.index = viewIndex;
this._viewCache![viewIndex] = view;
setTimeout(() => scrollTo({ behavior: "auto", top: 0 }), 0);
}
view.lovelace = this.lovelace;

View File

@ -185,7 +185,7 @@ export class HuiSection extends ReactiveElement {
if (isStrategySection(sectionConfig)) {
isStrategy = true;
sectionConfig = await generateLovelaceSectionStrategy(
sectionConfig.strategy,
sectionConfig,
this.hass!
);
}

View File

@ -1,5 +1,6 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { clamp } from "../../../../common/number/clamp";
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
@ -144,7 +145,10 @@ export class AreaViewStrategy extends ReactiveElement {
});
}
// Take the full width if there is only one section to avoid misalignment between cards and header
// Allow between 2 and 3 columns (the max should be set to define the width of the header)
const maxColumns = clamp(sections.length, 2, 3);
// Take the full width if there is only one section to avoid narrow header on desktop
if (sections.length === 1) {
sections[0].column_span = 2;
}
@ -160,7 +164,7 @@ export class AreaViewStrategy extends ReactiveElement {
content: `## ${area.name}`,
},
},
max_columns: 2,
max_columns: maxColumns,
sections: sections,
badges: badges,
};

View File

@ -1,13 +1,14 @@
import { STATE_NOT_RUNNING } from "home-assistant-js-websocket";
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
import type { LovelaceViewRawConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import type { LovelaceStrategyEditor } from "../types";
import type {
AreaViewStrategyConfig,
EntitiesDisplay,
} from "./area-view-strategy";
import type { LovelaceStrategyEditor } from "../types";
import type { AreasViewStrategyConfig } from "./areas-overview-view-strategy";
import { computeAreaPath, getAreas } from "./helpers/areas-strategy-helper";
@ -30,6 +31,28 @@ export class AreasDashboardStrategy extends ReactiveElement {
config: AreasDashboardStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceConfig> {
if (hass.config.state === STATE_NOT_RUNNING) {
return {
views: [
{
type: "sections",
sections: [{ cards: [{ type: "starting" }] }],
},
],
};
}
if (hass.config.recovery_mode) {
return {
views: [
{
type: "sections",
sections: [{ cards: [{ type: "recovery-mode" }] }],
},
],
};
}
const areas = getAreas(
hass.areas,
config.areas_display?.hidden,

View File

@ -95,7 +95,7 @@ export class AreasOverviewViewStrategy extends ReactiveElement {
return {
type: "sections",
max_columns: 2,
max_columns: 3,
sections: areaSections,
};
}

View File

@ -1,10 +1,18 @@
import type {
LovelaceSectionConfig,
LovelaceStrategySectionConfig,
} from "../../../data/lovelace/config/section";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import type {
LovelaceConfig,
LovelaceDashboardStrategyConfig,
LovelaceRawConfig,
} from "../../../data/lovelace/config/types";
import { isStrategyDashboard } from "../../../data/lovelace/config/types";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type {
LovelaceStrategyViewConfig,
LovelaceViewConfig,
} from "../../../data/lovelace/config/view";
import { isStrategyView } from "../../../data/lovelace/config/view";
import type { AsyncReturnType, HomeAssistant } from "../../../types";
import { cleanLegacyStrategyConfig, isLegacyStrategy } from "./legacy-strategy";
@ -133,10 +141,11 @@ const generateStrategy = async <T extends LovelaceStrategyConfigType>(
};
export const generateLovelaceDashboardStrategy = async (
strategyConfig: LovelaceStrategyConfig,
config: LovelaceDashboardStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceConfig> =>
generateStrategy(
): Promise<LovelaceConfig> => {
const { strategy, ...base } = config;
const generated = await generateStrategy(
"dashboard",
(err) => ({
views: [
@ -151,15 +160,21 @@ export const generateLovelaceDashboardStrategy = async (
},
],
}),
strategyConfig,
strategy,
hass
);
return {
...base,
...generated,
};
};
export const generateLovelaceViewStrategy = async (
strategyConfig: LovelaceStrategyConfig,
config: LovelaceStrategyViewConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> =>
generateStrategy(
): Promise<LovelaceViewConfig> => {
const { strategy, ...base } = config;
const generated = await generateStrategy(
"view",
(err) => ({
cards: [
@ -169,15 +184,21 @@ export const generateLovelaceViewStrategy = async (
},
],
}),
strategyConfig,
strategy,
hass
);
return {
...base,
...generated,
};
};
export const generateLovelaceSectionStrategy = async (
strategyConfig: LovelaceStrategyConfig,
config: LovelaceStrategySectionConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> =>
generateStrategy(
): Promise<LovelaceSectionConfig> => {
const { strategy, ...base } = config;
const generated = await generateStrategy(
"section",
(err) => ({
cards: [
@ -187,9 +208,14 @@ export const generateLovelaceSectionStrategy = async (
},
],
}),
strategyConfig,
strategy,
hass
);
return {
...base,
...generated,
};
};
/**
* Find all references to strategies and replaces them with the generated output
@ -199,20 +225,20 @@ export const expandLovelaceConfigStrategies = async (
hass: HomeAssistant
): Promise<LovelaceConfig> => {
const newConfig = isStrategyDashboard(config)
? await generateLovelaceDashboardStrategy(config.strategy, hass)
? await generateLovelaceDashboardStrategy(config, hass)
: { ...config };
newConfig.views = await Promise.all(
newConfig.views.map(async (view) => {
const newView = isStrategyView(view)
? await generateLovelaceViewStrategy(view.strategy, hass)
? await generateLovelaceViewStrategy(view, hass)
: { ...view };
if (newView.sections) {
newView.sections = await Promise.all(
newView.sections.map(async (section) => {
const newSection = isStrategyView(section)
? await generateLovelaceSectionStrategy(section.strategy, hass)
? await generateLovelaceSectionStrategy(section, hass)
: { ...section };
return newSection;
})

View File

@ -233,10 +233,7 @@ export class HUIView extends ReactiveElement {
if (isStrategyView(viewConfig)) {
isStrategy = true;
viewConfig = await generateLovelaceViewStrategy(
viewConfig.strategy,
this.hass!
);
viewConfig = await generateLovelaceViewStrategy(viewConfig, this.hass!);
}
viewConfig = {

View File

@ -2902,7 +2902,8 @@
"device_consumption_energy": "Device energy consumption",
"selected_stat_intro": "Select the energy sensor that measures the device's energy usage in either of {unit}.",
"included_in_device": "Upstream device",
"included_in_device_helper": "If this device is already counted by another device (such as a smart switch measured by a smart breaker), selecting the upstream device prevents duplicate energy tracking."
"included_in_device_helper": "If this device is already counted by another device (such as a smart switch measured by a smart breaker), selecting the upstream device prevents duplicate energy tracking.",
"no_upstream_devices": "No eligible upstream devices"
}
}
},
@ -3395,8 +3396,8 @@
"high": "High"
},
"commands": {
"header": "Supported commands",
"low": "Needs work",
"header": "Language support",
"low": "Needs more work",
"ready": "Ready to be used",
"high": "Fully supported"
},
@ -3430,21 +3431,22 @@
},
"local": {
"title": "Installing add-ons",
"secondary": "The Whisper and Piper add-ons are being installed and configured based on your hardware.",
"secondary": "We are preparing your system for local voice processing.",
"failed_title": "Failed to install add-ons",
"failed_secondary": "We were unable to install the Whisper and Piper add-ons automatically for you. Read the documentation to learn how to install them.",
"failed_secondary": "We were unable to install the add-ons for speech-to-text and text-to-speech automatically for you. Read the documentation to learn how to install them.",
"not_supported_title": "Installation of add-ons is not supported on your system",
"not_supported_secondary": "Your system is not supported to automatically install a local TTS and STT provider. Learn how to set up local TTS and STT providers in the documentation.",
"local_pipeline": "Local Assistant",
"full_local_pipeline": "Full local Assistant",
"focused_local_pipeline": "Focused local Assistant",
"state": {
"installing_piper": "Installing Piper add-on",
"starting_piper": "Starting Piper add-on",
"installing_piper": "Installing Piper",
"starting_piper": "Starting Piper",
"setup_piper": "Setting up Piper",
"installing_faster-whisper": "Installing Whisper add-on",
"starting_faster-whisper": "Starting Whisper add-on",
"installing_faster-whisper": "Installing Whisper",
"starting_faster-whisper": "Starting Whisper",
"setup_faster-whisper": "Setting up Whisper",
"installing_speech-to-phrase": "Installing Speech-to-Phrase add-on",
"starting_speech-to-phrase": "Starting Speech-to-Phrase add-on",
"installing_speech-to-phrase": "Installing Speech-to-Phrase",
"starting_speech-to-phrase": "Starting Speech-to-Phrase",
"setup_speech-to-phrase": "Setting up Speech-to-Phrase",
"creating_pipeline": "Creating assistant"
},
@ -7160,12 +7162,17 @@
"large": "Large"
},
"show_seconds": "Display seconds",
"time_format": "Time format",
"time_format": "[%key:ui::panel::profile::time_format::dropdown_label%]",
"time_formats": {
"auto": "Use user settings",
"language": "[%key:ui::panel::profile::time_format::formats::language%]",
"system": "[%key:ui::panel::profile::time_format::formats::system%]",
"24": "[%key:ui::panel::profile::time_format::formats::24%]",
"12": "[%key:ui::panel::profile::time_format::formats::12%]"
},
"time_zone": "[%key:ui::panel::profile::time_zone::dropdown_label%]",
"time_zones": {
"auto": "Use user settings"
}
},
"media-control": {
@ -8129,7 +8136,7 @@
},
"mean_type": {
"0": "None",
"1": "Arimethic",
"1": "Arithmetic",
"2": "Circular"
},
"fix_issue": {