Compare commits

...

41 Commits

Author SHA1 Message Date
Bram Kragten
0bcaa104e7 Bumped version to 20250401.0 2025-04-01 17:30:31 +02:00
Bram Kragten
6b3f807129 Developer tools action fixes (#24876) 2025-04-01 17:30:16 +02:00
Paul Bottein
c464d344db Add ellipsis for more info breadcrumb (#24882) 2025-04-01 17:29:14 +02:00
karwosts
69f0a4a728 Fix condition rendering in trace choose node (#24878) 2025-04-01 17:29:13 +02:00
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 9cfcd21a93.
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
57 changed files with 971 additions and 415 deletions

View File

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

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20250326.0" version = "20250401.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

@@ -6,6 +6,10 @@ import {
differenceInMilliseconds, differenceInMilliseconds,
differenceInMonths, differenceInMonths,
endOfMonth, endOfMonth,
startOfDay,
endOfDay,
differenceInDays,
addDays,
} from "date-fns"; } from "date-fns";
import { toZonedTime, fromZonedTime } from "date-fns-tz"; import { toZonedTime, fromZonedTime } from "date-fns-tz";
import type { HassConfig } from "home-assistant-js-websocket"; import type { HassConfig } from "home-assistant-js-websocket";
@@ -100,6 +104,32 @@ export const shiftDateRange = (
locale, locale,
config 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 { } else {
const difference = const difference =
((calcDateDifferenceProperty( ((calcDateDifferenceProperty(

View File

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

View File

@@ -1,7 +1,11 @@
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import type { AreaRegistryEntry } from "../../data/area_registry"; import type { AreaRegistryEntry } from "../../data/area_registry";
import type { DeviceRegistryEntry } from "../../data/device_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 { FloorRegistryEntry } from "../../data/floor_registry";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
@@ -19,6 +23,23 @@ export const getEntityContext = (
| EntityRegistryDisplayEntry | EntityRegistryDisplayEntry
| undefined; | 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 deviceId = entry?.device_id;
const device = deviceId ? hass.devices[deviceId] : null; const device = deviceId ? hass.devices[deviceId] : null;
const areaId = entry?.area_id || device?.area_id; 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 { mdiAlertOctagram, mdiCheckBold } from "@mdi/js";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
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 "../ha-button";
import "../ha-spinner"; import "../ha-spinner";
import "../ha-svg-icon"; import "../ha-svg-icon";
@customElement("ha-progress-button") @customElement("ha-progress-button")
export class HaProgressButton extends LitElement { export class HaProgressButton extends LitElement {
@property() public label?: string;
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public progress = false; @property({ type: Boolean }) public progress = false;
@@ -21,14 +23,16 @@ export class HaProgressButton extends LitElement {
public render(): TemplateResult { public render(): TemplateResult {
const overlay = this._result || this.progress; const overlay = this._result || this.progress;
return html` return html`
<mwc-button <ha-button
?raised=${this.raised} .raised=${this.raised}
.label=${this.label}
.unelevated=${this.unelevated} .unelevated=${this.unelevated}
.disabled=${this.disabled || this.progress} .disabled=${this.disabled || this.progress}
class=${this._result || ""} class=${this._result || ""}
> >
<slot name="icon" slot="icon"></slot>
<slot></slot> <slot></slot>
</mwc-button> </ha-button>
${!overlay ${!overlay
? nothing ? nothing
: html` : html`
@@ -68,12 +72,12 @@ export class HaProgressButton extends LitElement {
pointer-events: none; pointer-events: none;
} }
mwc-button { ha-button {
transition: all 1s; transition: all 1s;
pointer-events: initial; pointer-events: initial;
} }
mwc-button.success { ha-button.success {
--mdc-theme-primary: white; --mdc-theme-primary: white;
background-color: var(--success-color); background-color: var(--success-color);
transition: none; transition: none;
@@ -81,13 +85,13 @@ export class HaProgressButton extends LitElement {
pointer-events: none; pointer-events: none;
} }
mwc-button[unelevated].success, ha-button[unelevated].success,
mwc-button[raised].success { ha-button[raised].success {
--mdc-theme-primary: var(--success-color); --mdc-theme-primary: var(--success-color);
--mdc-theme-on-primary: white; --mdc-theme-on-primary: white;
} }
mwc-button.error { ha-button.error {
--mdc-theme-primary: white; --mdc-theme-primary: white;
background-color: var(--error-color); background-color: var(--error-color);
transition: none; transition: none;
@@ -95,8 +99,8 @@ export class HaProgressButton extends LitElement {
pointer-events: none; pointer-events: none;
} }
mwc-button[unelevated].error, ha-button[unelevated].error,
mwc-button[raised].error { ha-button[raised].error {
--mdc-theme-primary: var(--error-color); --mdc-theme-primary: var(--error-color);
--mdc-theme-on-primary: white; --mdc-theme-on-primary: white;
} }
@@ -113,8 +117,8 @@ export class HaProgressButton extends LitElement {
color: white; color: white;
} }
mwc-button.success slot, ha-button.success slot,
mwc-button.error slot { ha-button.error slot {
visibility: hidden; visibility: hidden;
} }
:host([destructive]) { :host([destructive]) {

View File

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

View File

@@ -211,36 +211,12 @@ export class HaRelatedItems extends LitElement {
)} )}
</mwc-list>` </mwc-list>`
: nothing} : 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 ${this._related.area
? html`<h3> ? html`<h3>
${this.hass.localize("ui.components.related-items.area")} ${this.hass.localize("ui.components.related-items.area")}
</h3> </h3>
<mwc-list <mwc-list>
>${this._related.area.map((relatedAreaId) => { ${this._related.area.map((relatedAreaId) => {
const area = this.hass.areas[relatedAreaId]; const area = this.hass.areas[relatedAreaId];
if (!area) { if (!area) {
return nothing; return nothing;
@@ -268,8 +244,33 @@ export class HaRelatedItems extends LitElement {
</ha-list-item> </ha-list-item>
</a> </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} : nothing}
${this._related.entity ${this._related.entity
? html` ? html`

View File

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

View File

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

View File

@@ -186,7 +186,10 @@ export class HatScriptGraph extends LitElement {
? ensureArray(config.choose)?.map((branch, i) => { ? ensureArray(config.choose)?.map((branch, i) => {
const branchPath = `${path}/choose/${i}`; const branchPath = `${path}/choose/${i}`;
const trackThis = tracePath.includes(i); const trackThis = tracePath.includes(i);
this.renderedNodes[branchPath] = { config, path: branchPath }; this.renderedNodes[branchPath] = {
config: branch,
path: branchPath,
};
if (trackThis) { if (trackThis) {
this.trackedNodes[branchPath] = this.renderedNodes[branchPath]; this.trackedNodes[branchPath] = this.renderedNodes[branchPath];
} }
@@ -196,7 +199,7 @@ export class HatScriptGraph extends LitElement {
.iconPath=${!trace || trackThis .iconPath=${!trace || trackThis
? mdiCheckboxMarkedOutline ? mdiCheckboxMarkedOutline
: mdiCheckboxBlankOutline} : mdiCheckboxBlankOutline}
@focus=${this._selectNode(config, branchPath)} @focus=${this._selectNode(branch, branchPath)}
?track=${trackThis} ?track=${trackThis}
?active=${this.selected === branchPath} ?active=${this.selected === branchPath}
.notEnabled=${disabled || config.enabled === false} .notEnabled=${disabled || config.enabled === false}

View File

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

View File

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

View File

@@ -21,8 +21,10 @@ import { stopPropagation } from "../../common/dom/stop_propagation";
import { computeAreaName } from "../../common/entity/compute_area_name"; import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceName } from "../../common/entity/compute_device_name"; import { computeDeviceName } from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { computeEntityName } from "../../common/entity/compute_entity_name"; import {
import { getEntityContext } from "../../common/entity/get_entity_context"; computeEntityEntryName,
computeEntityName,
} from "../../common/entity/compute_entity_name";
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event"; import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
import { navigate } from "../../common/navigate"; import { navigate } from "../../common/navigate";
import "../../components/ha-button-menu"; 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-info";
import "./ha-more-info-settings"; import "./ha-more-info-settings";
import "./more-info-content"; import "./more-info-content";
import {
getEntityContext,
getEntityEntryContext,
} from "../../common/entity/get_entity_context";
export interface MoreInfoDialogParams { export interface MoreInfoDialogParams {
entityId: string | null; entityId: string | null;
@@ -270,6 +276,11 @@ export class MoreInfoDialog extends LitElement {
this._setView("related"); this._setView("related");
} }
private _breadcrumbClick(ev: Event) {
ev.stopPropagation();
this._setView("related");
}
private async _loadNumericDeviceClasses() { private async _loadNumericDeviceClasses() {
const deviceClasses = await getSensorNumericDeviceClasses(this.hass); const deviceClasses = await getSensorNumericDeviceClasses(this.hass);
this._sensorNumericDeviceClasses = deviceClasses.numeric_device_classes; this._sensorNumericDeviceClasses = deviceClasses.numeric_device_classes;
@@ -293,11 +304,18 @@ export class MoreInfoDialog extends LitElement {
this._initialView !== DEFAULT_VIEW && !this._childView; this._initialView !== DEFAULT_VIEW && !this._childView;
const showCloseIcon = isDefaultView || isSpecificInitialView; 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 const entityName = stateObj
? computeEntityName(stateObj, this.hass) ? computeEntityName(stateObj, this.hass)
: undefined; : this._entry
? computeEntityEntryName(this._entry, this.hass)
: entityId;
const deviceName = context?.device const deviceName = context?.device
? computeDeviceName(context.device) ? computeDeviceName(context.device)
: undefined; : undefined;
@@ -306,7 +324,7 @@ export class MoreInfoDialog extends LitElement {
const breadcrumb = [areaName, deviceName, entityName].filter( const breadcrumb = [areaName, deviceName, entityName].filter(
(v): v is string => Boolean(v) (v): v is string => Boolean(v)
); );
const title = this._childView?.viewTitle || breadcrumb.pop(); const title = this._childView?.viewTitle || breadcrumb.pop() || entityId;
return html` return html`
<ha-dialog <ha-dialog
@@ -337,14 +355,19 @@ export class MoreInfoDialog extends LitElement {
)} )}
></ha-icon-button-prev> ></ha-icon-button-prev>
`} `}
<span <span slot="title" @click=${this._enlarge} class="title">
slot="title"
.title=${title}
@click=${this._enlarge}
class="title"
>
${breadcrumb.length > 0 ${breadcrumb.length > 0
? !__DEMO__ && isAdmin
? html` ? 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"> <p class="breadcrumb">
${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)} ${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)}
</p> </p>
@@ -643,6 +666,7 @@ export class MoreInfoDialog extends LitElement {
.title { .title {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start;
} }
.title p { .title p {
@@ -663,11 +687,30 @@ export class MoreInfoDialog extends LitElement {
color: var(--secondary-text-color); color: var(--secondary-text-color);
font-size: 14px; font-size: 14px;
line-height: 16px; 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;
border-radius: 6px;
transition: background-color 180ms ease-in-out;
min-width: 0;
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
text-align: left;
} }
.title .breadcrumb { .title button.breadcrumb {
--mdc-icon-size: 16px; 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 { customElement, property, query, state } from "lit/decorators";
import { storage } from "../../common/decorators/storage"; import { storage } from "../../common/decorators/storage";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button";
import { createCloseHeading } from "../../components/ha-dialog"; import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-textarea"; import "../../components/ha-textarea";
import type { HaTextArea } from "../../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 type { HomeAssistant } from "../../types";
import { showAlertDialog } from "../generic/show-dialog-box"; import { showAlertDialog } from "../generic/show-dialog-box";
import type { TTSTryDialogParams } from "./show-dialog-tts-try"; import type { TTSTryDialogParams } from "./show-dialog-tts-try";
import "../../components/ha-spinner"; import "../../components/buttons/ha-progress-button";
@customElement("dialog-tts-try") @customElement("dialog-tts-try")
export class TTSTryDialog extends LitElement { export class TTSTryDialog extends LitElement {
@@ -81,28 +80,17 @@ export class TTSTryDialog extends LitElement {
?dialogInitialFocus=${!this._defaultMessage} ?dialogInitialFocus=${!this._defaultMessage}
> >
</ha-textarea> </ha-textarea>
${this._loadingExample
? html` <ha-progress-button
<ha-spinner .progress=${this._loadingExample}
size="small"
slot="primaryAction"
class="loading"
></ha-spinner>
`
: html`
<ha-button
?dialogInitialFocus=${Boolean(this._defaultMessage)} ?dialogInitialFocus=${Boolean(this._defaultMessage)}
slot="primaryAction" slot="primaryAction"
.label=${this.hass.localize("ui.dialogs.tts-try.play")} .label=${this.hass.localize("ui.dialogs.tts-try.play")}
@click=${this._playExample} @click=${this._playExample}
.disabled=${!this._valid} .disabled=${!this._valid}
> >
<ha-svg-icon <ha-svg-icon slot="icon" .path=${mdiPlayCircleOutline}></ha-svg-icon>
slot="icon" </ha-progress-button>
.path=${mdiPlayCircleOutline}
></ha-svg-icon>
</ha-button>
`}
</ha-dialog> </ha-dialog>
`; `;
} }

View File

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

View File

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

View File

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

View File

@@ -152,12 +152,14 @@ export class EnergyDeviceSettings extends LitElement {
device_consumptions: this.preferences device_consumptions: this.preferences
.device_consumption as DeviceConsumptionEnergyPreference[], .device_consumption as DeviceConsumptionEnergyPreference[],
saveCallback: async (newDevice) => { saveCallback: async (newDevice) => {
await this._savePreferences({ const newPrefs = {
...this.preferences, ...this.preferences,
device_consumption: this.preferences.device_consumption.map((d) => device_consumption: this.preferences.device_consumption.map((d) =>
d === origDevice ? newDevice : 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) { private async _deleteDevice(ev) {
const deviceToDelete: DeviceConsumptionEnergyPreference = const deviceToDelete: DeviceConsumptionEnergyPreference =
ev.currentTarget.device; ev.currentTarget.device;
@@ -196,14 +207,7 @@ export class EnergyDeviceSettings extends LitElement {
(device) => device !== deviceToDelete (device) => device !== deviceToDelete
), ),
}; };
newPrefs.device_consumption.forEach((d, idx) => { this._sanitizeParents(newPrefs);
if (d.included_in_stat === deviceToDelete.stat_consumption) {
newPrefs.device_consumption[idx] = {
...newPrefs.device_consumption[idx],
};
delete newPrefs.device_consumption[idx].included_in_stat;
}
});
await this._savePreferences(newPrefs); await this._savePreferences(newPrefs);
} catch (err: any) { } catch (err: any) {
showAlertDialog(this, { title: `Failed to save config: ${err.message}` }); 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( this._possibleParents = this._params.device_consumptions.filter(
(d) => (d) =>
d.stat_consumption !== this._device!.stat_consumption && d.stat_consumption !== this._device!.stat_consumption &&
d.stat_consumption !== this._params?.device?.stat_consumption &&
!children.includes(d.stat_consumption) !children.includes(d.stat_consumption)
); );
} }
@@ -160,7 +161,15 @@ export class DialogEnergyDeviceSettings
naturalMenuWidth naturalMenuWidth
clearable clearable
> >
${this._possibleParents.map( ${!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` (stat) => html`
<mwc-list-item .value=${stat.stat_consumption} <mwc-list-item .value=${stat.stat_consumption}
>${stat.name || >${stat.name ||

View File

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

View File

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

View File

@@ -511,7 +511,20 @@ class HaPanelDevAction extends LitElement {
return; return;
} }
this._yamlValid = true; this._yamlValid = true;
this._serviceDataChanged(ev);
if (typeof ev.detail.value !== "object") {
return;
}
if (this._serviceData?.action !== ev.detail.value.action) {
this._error = undefined;
}
this._serviceData = migrateAutomationAction(
ev.detail.value
) as ServiceAction;
this._checkUiSupported();
} }
private _checkUiSupported() { private _checkUiSupported() {
@@ -547,18 +560,18 @@ class HaPanelDevAction extends LitElement {
if (this._serviceData?.action !== ev.detail.value.action) { if (this._serviceData?.action !== ev.detail.value.action) {
this._error = undefined; this._error = undefined;
} }
this._serviceData = migrateAutomationAction( this._serviceData = ev.detail.value;
ev.detail.value
) as ServiceAction;
this._checkUiSupported(); this._checkUiSupported();
} }
private _serviceChanged(ev) { private _serviceChanged(ev) {
ev.stopPropagation(); ev.stopPropagation();
this._serviceData = { action: ev.detail.value || "", data: {} }; if (ev.detail.value) {
this._serviceData = { action: ev.detail.value, data: {} };
this._yamlEditor?.setValue(this._serviceData);
}
this._response = undefined; this._response = undefined;
this._error = undefined; this._error = undefined;
this._yamlEditor?.setValue(this._serviceData);
this._checkUiSupported(); this._checkUiSupported();
} }

View File

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

View File

@@ -65,7 +65,9 @@ export class HuiClockCard extends LitElement implements LovelaceCard {
minute: "2-digit", minute: "2-digit",
second: "2-digit", second: "2-digit",
hourCycle: useAmPm(locale) ? "h12" : "h23", 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(); this._tick();
@@ -79,7 +81,7 @@ export class HuiClockCard extends LitElement implements LovelaceCard {
public getGridOptions(): LovelaceGridOptions { public getGridOptions(): LovelaceGridOptions {
if (this._config?.clock_size === "medium") { if (this._config?.clock_size === "medium") {
return { return {
min_rows: 1, min_rows: this._config?.title ? 2 : 1,
rows: 2, rows: 2,
max_rows: 4, max_rows: 4,
min_columns: 4, min_columns: 4,
@@ -101,7 +103,7 @@ export class HuiClockCard extends LitElement implements LovelaceCard {
min_rows: 1, min_rows: 1,
rows: 1, rows: 1,
max_rows: 4, max_rows: 4,
min_columns: 4, min_columns: 3,
columns: 6, columns: 6,
}; };
} }
@@ -160,6 +162,9 @@ export class HuiClockCard extends LitElement implements LovelaceCard {
? `size-${this._config.clock_size}` ? `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-parts">
<div class="time-part hour">${this._timeHour}</div> <div class="time-part hour">${this._timeHour}</div>
<div class="time-part minute">${this._timeMinute}</div> <div class="time-part minute">${this._timeMinute}</div>
@@ -182,9 +187,41 @@ export class HuiClockCard extends LitElement implements LovelaceCard {
.time-wrapper { .time-wrapper {
display: flex; display: flex;
height: 100%; height: calc(100% - 12px);
align-items: center; align-items: center;
flex-direction: column;
justify-content: center; 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 { .time-parts {
@@ -197,7 +234,11 @@ export class HuiClockCard extends LitElement implements LovelaceCard {
font-size: 2rem; font-size: 2rem;
font-weight: 500; font-weight: 500;
line-height: 0.8; line-height: 0.8;
padding: 16px 0; direction: ltr;
}
.time-title + .time-parts {
font-size: 1.5rem;
} }
.time-wrapper.size-medium .time-parts { .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.second,
.time-parts .time-part.am-pm { .time-parts .time-part.am-pm {
font-size: 12px; font-size: 10px;
font-weight: 500;
margin-left: 4px; margin-left: 4px;
} }

View File

@@ -46,7 +46,10 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
throw new Error("Image required"); throw new Error("Image required");
} }
this._config = config; this._config = {
tap_action: { action: "more-info" },
...config,
};
} }
protected shouldUpdate(changedProps: PropertyValues): boolean { 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 { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/ha-card"; import "../../../components/ha-card";
import type { CameraEntity } from "../../../data/camera";
import type { ImageEntity } from "../../../data/image"; import type { ImageEntity } from "../../../data/image";
import { computeImageUrl } from "../../../data/image"; import { computeImageUrl } from "../../../data/image";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import type { PersonEntity } from "../../../data/person";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive"; import { actionHandler } from "../common/directives/action-handler-directive";
import { findEntities } from "../common/find-entities"; import { findEntities } from "../common/find-entities";
@@ -19,8 +21,6 @@ import "../components/hui-image";
import { createEntityNotFoundWarning } from "../components/hui-warning"; import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { LovelaceCard, LovelaceCardEditor } from "../types"; import type { LovelaceCard, LovelaceCardEditor } from "../types";
import type { PictureEntityCardConfig } from "./types"; import type { PictureEntityCardConfig } from "./types";
import type { CameraEntity } from "../../../data/camera";
import type { PersonEntity } from "../../../data/person";
export const STUB_IMAGE = export const STUB_IMAGE =
"https://demo.home-assistant.io/stub_config/bedroom.png"; "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"); 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 { protected shouldUpdate(changedProps: PropertyValues): boolean {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
import timezones from "google-timezones-json";
import { html, LitElement, nothing } from "lit"; import { 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 memoizeOne from "memoize-one";
@@ -9,6 +10,7 @@ import {
literal, literal,
object, object,
optional, optional,
string,
union, union,
} from "superstruct"; } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
@@ -27,10 +29,12 @@ import { TimeFormat } from "../../../../data/translation";
const cardConfigStruct = assign( const cardConfigStruct = assign(
baseLovelaceCardConfig, baseLovelaceCardConfig,
object({ object({
title: optional(string()),
clock_size: optional( clock_size: optional(
union([literal("small"), literal("medium"), literal("large")]) union([literal("small"), literal("medium"), literal("large")])
), ),
time_format: optional(enums(Object.values(TimeFormat))), time_format: optional(enums(Object.values(TimeFormat))),
time_zone: optional(enums(Object.keys(timezones))),
show_seconds: optional(boolean()), show_seconds: optional(boolean()),
}) })
); );
@@ -47,6 +51,7 @@ export class HuiClockCardEditor
private _schema = memoizeOne( private _schema = memoizeOne(
(localize: LocalizeFunc) => (localize: LocalizeFunc) =>
[ [
{ name: "title", selector: { text: {} } },
{ {
name: "clock_size", name: "clock_size",
selector: { selector: {
@@ -61,18 +66,13 @@ export class HuiClockCardEditor
}, },
}, },
}, },
{ { name: "show_seconds", selector: { boolean: {} } },
name: "show_seconds",
selector: {
boolean: {},
},
},
{ {
name: "time_format", name: "time_format",
selector: { selector: {
select: { select: {
mode: "dropdown", mode: "dropdown",
options: Object.values(TimeFormat).map((value) => ({ options: ["auto", ...Object.values(TimeFormat)].map((value) => ({
value, value,
label: localize( label: localize(
`ui.panel.lovelace.editor.card.clock.time_formats.${value}` `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[] ] as const satisfies readonly HaFormSchema[]
); );
private _data = memoizeOne((config) => ({ private _data = memoizeOne((config) => ({
clock_size: "small", clock_size: "small",
time_format: TimeFormat.language, time_zone: "auto",
time_format: "auto",
show_seconds: false, show_seconds: false,
...config, ...config,
})); }));
@@ -113,6 +134,13 @@ export class HuiClockCardEditor
} }
private _valueChanged(ev: CustomEvent): void { 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 }); fireEvent(this, "config-changed", { config: ev.detail.value });
} }
@@ -120,6 +148,10 @@ export class HuiClockCardEditor
schema: SchemaUnion<ReturnType<typeof this._schema>> schema: SchemaUnion<ReturnType<typeof this._schema>>
) => { ) => {
switch (schema.name) { switch (schema.name) {
case "title":
return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.title"
);
case "clock_size": case "clock_size":
return this.hass!.localize( return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.clock_size` `ui.panel.lovelace.editor.card.clock.clock_size`
@@ -128,6 +160,10 @@ export class HuiClockCardEditor
return this.hass!.localize( return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.time_format` `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": case "show_seconds":
return this.hass!.localize( return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.show_seconds` `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 { 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 memoizeOne from "memoize-one";
@@ -16,14 +17,21 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form"; import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types"; import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { DEFAULT_MAX, DEFAULT_MIN } from "../../cards/hui-gauge-card";
import type { GaugeCardConfig } from "../../cards/types"; import type { GaugeCardConfig } from "../../cards/types";
import type { UiAction } from "../../components/hui-action-editor";
import type { LovelaceCardEditor } from "../../types"; import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct"; import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-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({ const gaugeSegmentStruct = object({
from: number(), from: number(),
@@ -133,6 +141,12 @@ export class HuiGaugeCardEditor
}, },
] as const) ] as const)
: []), : []),
{
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{ {
name: "tap_action", name: "tap_action",
selector: { selector: {
@@ -142,6 +156,24 @@ export class HuiGaugeCardEditor
}, },
}, },
}, },
{
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 ] as const
); );
@@ -231,7 +263,13 @@ export class HuiGaugeCardEditor
return this.hass!.localize( return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.unit" "ui.panel.lovelace.editor.card.generic.unit"
); );
case "interactions":
return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.interactions"
);
case "tap_action": case "tap_action":
case "hold_action":
case "double_tap_action":
return `${this.hass!.localize( return `${this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}` `ui.panel.lovelace.editor.card.generic.${schema.name}`
)} (${this.hass!.localize( )} (${this.hass!.localize(

View File

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

View File

@@ -1,3 +1,4 @@
import { mdiGestureTap } from "@mdi/js";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct"; import { assert, assign, object, optional, string } from "superstruct";
@@ -18,6 +19,7 @@ const cardConfigStruct = assign(
image_entity: optional(string()), image_entity: optional(string()),
tap_action: optional(actionConfigStruct), tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct), hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
theme: optional(string()), theme: optional(string()),
alt_text: optional(string()), alt_text: optional(string()),
}) })
@@ -31,13 +33,36 @@ const SCHEMA = [
}, },
{ name: "alt_text", selector: { text: {} } }, { name: "alt_text", selector: { text: {} } },
{ name: "theme", selector: { theme: {} } }, { name: "theme", selector: { theme: {} } },
{
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{ {
name: "tap_action", name: "tap_action",
selector: { ui_action: {} }, selector: {
ui_action: {
default_action: "more-info",
},
},
}, },
{ {
name: "hold_action", name: "",
selector: { ui_action: {} }, 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

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

View File

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

View File

@@ -1,3 +1,4 @@
import { mdiGestureTap } from "@mdi/js";
import { html, LitElement, nothing } from "lit"; import { 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 memoizeOne from "memoize-one";
@@ -5,12 +6,13 @@ import {
assert, assert,
assign, assign,
boolean, boolean,
number,
object, object,
optional, optional,
string, string,
number,
} from "superstruct"; } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../../common/translations/localize"; import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form"; import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types"; import type { SchemaUnion } from "../../../../components/ha-form/types";
@@ -22,7 +24,6 @@ import type { WeatherForecastCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types"; import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct"; import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { supportsFeature } from "../../../../common/entity/supports-feature";
const cardConfigStruct = assign( const cardConfigStruct = assign(
baseLovelaceCardConfig, baseLovelaceCardConfig,
@@ -239,6 +240,37 @@ export class HuiWeatherForecastCardEditor
selector: { number: { min: 1, max: 12 } }, selector: { number: { min: 1, max: 12 } },
default: 5, 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)
: []), : []),
] as const ] as const

View File

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

View File

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

View File

@@ -60,7 +60,7 @@ export class HuiStateBadgeElement
throw Error("Entity required"); 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 { protected shouldUpdate(changedProps: PropertyValues): boolean {

View File

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

View File

@@ -56,7 +56,7 @@ class HuiStateLabelElement extends LitElement implements LovelaceElement {
throw Error("Entity required"); 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 { protected shouldUpdate(changedProps: PropertyValues): boolean {

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import { ReactiveElement } from "lit"; import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { clamp } from "../../../../common/number/clamp";
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge"; import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card"; import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section"; 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) { if (sections.length === 1) {
sections[0].column_span = 2; sections[0].column_span = 2;
} }
@@ -160,7 +164,7 @@ export class AreaViewStrategy extends ReactiveElement {
content: `## ${area.name}`, content: `## ${area.name}`,
}, },
}, },
max_columns: 2, max_columns: maxColumns,
sections: sections, sections: sections,
badges: badges, badges: badges,
}; };

View File

@@ -1,13 +1,14 @@
import { STATE_NOT_RUNNING } from "home-assistant-js-websocket";
import { ReactiveElement } from "lit"; import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import type { LovelaceConfig } from "../../../../data/lovelace/config/types"; import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
import type { LovelaceViewRawConfig } from "../../../../data/lovelace/config/view"; import type { LovelaceViewRawConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import type { LovelaceStrategyEditor } from "../types";
import type { import type {
AreaViewStrategyConfig, AreaViewStrategyConfig,
EntitiesDisplay, EntitiesDisplay,
} from "./area-view-strategy"; } from "./area-view-strategy";
import type { LovelaceStrategyEditor } from "../types";
import type { AreasViewStrategyConfig } from "./areas-overview-view-strategy"; import type { AreasViewStrategyConfig } from "./areas-overview-view-strategy";
import { computeAreaPath, getAreas } from "./helpers/areas-strategy-helper"; import { computeAreaPath, getAreas } from "./helpers/areas-strategy-helper";
@@ -30,6 +31,28 @@ export class AreasDashboardStrategy extends ReactiveElement {
config: AreasDashboardStrategyConfig, config: AreasDashboardStrategyConfig,
hass: HomeAssistant hass: HomeAssistant
): Promise<LovelaceConfig> { ): 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( const areas = getAreas(
hass.areas, hass.areas,
config.areas_display?.hidden, config.areas_display?.hidden,

View File

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

View File

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

View File

@@ -2902,7 +2902,8 @@
"device_consumption_energy": "Device energy consumption", "device_consumption_energy": "Device energy consumption",
"selected_stat_intro": "Select the energy sensor that measures the device's energy usage in either of {unit}.", "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": "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" "high": "High"
}, },
"commands": { "commands": {
"header": "Supported commands", "header": "Language support",
"low": "Needs work", "low": "Needs more work",
"ready": "Ready to be used", "ready": "Ready to be used",
"high": "Fully supported" "high": "Fully supported"
}, },
@@ -3430,21 +3431,22 @@
}, },
"local": { "local": {
"title": "Installing add-ons", "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_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_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.", "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": { "state": {
"installing_piper": "Installing Piper add-on", "installing_piper": "Installing Piper",
"starting_piper": "Starting Piper add-on", "starting_piper": "Starting Piper",
"setup_piper": "Setting up Piper", "setup_piper": "Setting up Piper",
"installing_faster-whisper": "Installing Whisper add-on", "installing_faster-whisper": "Installing Whisper",
"starting_faster-whisper": "Starting Whisper add-on", "starting_faster-whisper": "Starting Whisper",
"setup_faster-whisper": "Setting up Whisper", "setup_faster-whisper": "Setting up Whisper",
"installing_speech-to-phrase": "Installing Speech-to-Phrase add-on", "installing_speech-to-phrase": "Installing Speech-to-Phrase",
"starting_speech-to-phrase": "Starting Speech-to-Phrase add-on", "starting_speech-to-phrase": "Starting Speech-to-Phrase",
"setup_speech-to-phrase": "Setting up Speech-to-Phrase", "setup_speech-to-phrase": "Setting up Speech-to-Phrase",
"creating_pipeline": "Creating assistant" "creating_pipeline": "Creating assistant"
}, },
@@ -7160,12 +7162,17 @@
"large": "Large" "large": "Large"
}, },
"show_seconds": "Display seconds", "show_seconds": "Display seconds",
"time_format": "Time format", "time_format": "[%key:ui::panel::profile::time_format::dropdown_label%]",
"time_formats": { "time_formats": {
"auto": "Use user settings",
"language": "[%key:ui::panel::profile::time_format::formats::language%]", "language": "[%key:ui::panel::profile::time_format::formats::language%]",
"system": "[%key:ui::panel::profile::time_format::formats::system%]", "system": "[%key:ui::panel::profile::time_format::formats::system%]",
"24": "[%key:ui::panel::profile::time_format::formats::24%]", "24": "[%key:ui::panel::profile::time_format::formats::24%]",
"12": "[%key:ui::panel::profile::time_format::formats::12%]" "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": { "media-control": {
@@ -8129,7 +8136,7 @@
}, },
"mean_type": { "mean_type": {
"0": "None", "0": "None",
"1": "Arimethic", "1": "Arithmetic",
"2": "Circular" "2": "Circular"
}, },
"fix_issue": { "fix_issue": {