Merge branch 'dev' into break-out-assist-chat

This commit is contained in:
Paulus Schoutsen 2024-09-03 14:15:43 -04:00 committed by GitHub
commit 9ff3218964
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
121 changed files with 2190 additions and 944 deletions

View File

@ -89,7 +89,7 @@ jobs:
env: env:
IS_TEST: "true" IS_TEST: "true"
- name: Upload bundle stats - name: Upload bundle stats
uses: actions/upload-artifact@v4.3.6 uses: actions/upload-artifact@v4.4.0
with: with:
name: frontend-bundle-stats name: frontend-bundle-stats
path: build/stats/*.json path: build/stats/*.json
@ -113,7 +113,7 @@ jobs:
env: env:
IS_TEST: "true" IS_TEST: "true"
- name: Upload bundle stats - name: Upload bundle stats
uses: actions/upload-artifact@v4.3.6 uses: actions/upload-artifact@v4.4.0
with: with:
name: supervisor-bundle-stats name: supervisor-bundle-stats
path: build/stats/*.json path: build/stats/*.json

View File

@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v4.3.6 uses: actions/upload-artifact@v4.4.0
with: with:
name: wheels name: wheels
path: dist/home_assistant_frontend*.whl path: dist/home_assistant_frontend*.whl
if-no-files-found: error if-no-files-found: error
- name: Upload translations - name: Upload translations
uses: actions/upload-artifact@v4.3.6 uses: actions/upload-artifact@v4.4.0
with: with:
name: translations name: translations
path: translations.tar.gz path: translations.tar.gz

File diff suppressed because one or more lines are too long

View File

@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.4.0.cjs yarnPath: .yarn/releases/yarn-4.4.1.cjs

View File

@ -11,7 +11,6 @@ import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervis
import type { ConditionWithShorthand } from "../../../../src/data/automation"; import type { ConditionWithShorthand } from "../../../../src/data/automation";
import "../../../../src/panels/config/automation/condition/ha-automation-condition"; import "../../../../src/panels/config/automation/condition/ha-automation-condition";
import { HaDeviceCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-device"; import { HaDeviceCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-device";
import { HaLogicalCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-logical";
import HaNumericStateCondition from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-numeric_state"; import HaNumericStateCondition from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-numeric_state";
import { HaStateCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-state"; import { HaStateCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-state";
import { HaSunCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-sun"; import { HaSunCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-sun";
@ -19,62 +18,67 @@ import { HaTemplateCondition } from "../../../../src/panels/config/automation/co
import { HaTimeCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-time"; import { HaTimeCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-time";
import { HaTriggerCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-trigger"; import { HaTriggerCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-trigger";
import { HaZoneCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-zone"; import { HaZoneCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-zone";
import { HaAndCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-and";
import { HaOrCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-or";
import { HaNotCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-not";
const SCHEMAS: { name: string; conditions: ConditionWithShorthand[] }[] = [ const SCHEMAS: { name: string; conditions: ConditionWithShorthand[] }[] = [
{ {
name: "State", name: "State",
conditions: [{ condition: "state", ...HaStateCondition.defaultConfig }], conditions: [{ ...HaStateCondition.defaultConfig }],
}, },
{ {
name: "Numeric State", name: "Numeric State",
conditions: [ conditions: [{ ...HaNumericStateCondition.defaultConfig }],
{ condition: "numeric_state", ...HaNumericStateCondition.defaultConfig },
],
}, },
{ {
name: "Sun", name: "Sun",
conditions: [{ condition: "sun", ...HaSunCondition.defaultConfig }], conditions: [{ ...HaSunCondition.defaultConfig }],
}, },
{ {
name: "Zone", name: "Zone",
conditions: [{ condition: "zone", ...HaZoneCondition.defaultConfig }], conditions: [{ ...HaZoneCondition.defaultConfig }],
}, },
{ {
name: "Time", name: "Time",
conditions: [{ condition: "time", ...HaTimeCondition.defaultConfig }], conditions: [{ ...HaTimeCondition.defaultConfig }],
}, },
{ {
name: "Template", name: "Template",
conditions: [ conditions: [{ ...HaTemplateCondition.defaultConfig }],
{ condition: "template", ...HaTemplateCondition.defaultConfig },
],
}, },
{ {
name: "Device", name: "Device",
conditions: [{ condition: "device", ...HaDeviceCondition.defaultConfig }], conditions: [{ ...HaDeviceCondition.defaultConfig }],
}, },
{ {
name: "And", name: "And",
conditions: [{ condition: "and", ...HaLogicalCondition.defaultConfig }], conditions: [{ ...HaAndCondition.defaultConfig }],
}, },
{ {
name: "Or", name: "Or",
conditions: [{ condition: "or", ...HaLogicalCondition.defaultConfig }], conditions: [{ ...HaOrCondition.defaultConfig }],
}, },
{ {
name: "Not", name: "Not",
conditions: [{ condition: "not", ...HaLogicalCondition.defaultConfig }], conditions: [{ ...HaNotCondition.defaultConfig }],
}, },
{ {
name: "Trigger", name: "Trigger",
conditions: [{ condition: "trigger", ...HaTriggerCondition.defaultConfig }], conditions: [{ ...HaTriggerCondition.defaultConfig }],
}, },
{ {
name: "Shorthand", name: "Shorthand",
conditions: [ conditions: [
{ and: HaLogicalCondition.defaultConfig.conditions }, {
{ or: HaLogicalCondition.defaultConfig.conditions }, ...HaAndCondition.defaultConfig,
{ not: HaLogicalCondition.defaultConfig.conditions }, },
{
...HaOrCondition.defaultConfig,
},
{
...HaNotCondition.defaultConfig,
},
], ],
}, },
]; ];

View File

@ -30,55 +30,48 @@ import { HaConversationTrigger } from "../../../../src/panels/config/automation/
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
{ {
name: "State", name: "State",
triggers: [{ platform: "state", ...HaStateTrigger.defaultConfig }], triggers: [{ ...HaStateTrigger.defaultConfig }],
}, },
{ {
name: "MQTT", name: "MQTT",
triggers: [{ platform: "mqtt", ...HaMQTTTrigger.defaultConfig }], triggers: [{ ...HaMQTTTrigger.defaultConfig }],
}, },
{ {
name: "GeoLocation", name: "GeoLocation",
triggers: [ triggers: [{ ...HaGeolocationTrigger.defaultConfig }],
{ platform: "geo_location", ...HaGeolocationTrigger.defaultConfig },
],
}, },
{ {
name: "Home Assistant", name: "Home Assistant",
triggers: [{ platform: "homeassistant", ...HaHassTrigger.defaultConfig }], triggers: [{ ...HaHassTrigger.defaultConfig }],
}, },
{ {
name: "Numeric State", name: "Numeric State",
triggers: [ triggers: [{ ...HaNumericStateTrigger.defaultConfig }],
{ platform: "numeric_state", ...HaNumericStateTrigger.defaultConfig },
],
}, },
{ {
name: "Sun", name: "Sun",
triggers: [{ platform: "sun", ...HaSunTrigger.defaultConfig }], triggers: [{ ...HaSunTrigger.defaultConfig }],
}, },
{ {
name: "Time Pattern", name: "Time Pattern",
triggers: [ triggers: [{ ...HaTimePatternTrigger.defaultConfig }],
{ platform: "time_pattern", ...HaTimePatternTrigger.defaultConfig },
],
}, },
{ {
name: "Webhook", name: "Webhook",
triggers: [{ platform: "webhook", ...HaWebhookTrigger.defaultConfig }], triggers: [{ ...HaWebhookTrigger.defaultConfig }],
}, },
{ {
name: "Persistent Notification", name: "Persistent Notification",
triggers: [ triggers: [
{ {
platform: "persistent_notification",
...HaPersistentNotificationTrigger.defaultConfig, ...HaPersistentNotificationTrigger.defaultConfig,
}, },
], ],
@ -86,37 +79,37 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
{ {
name: "Zone", name: "Zone",
triggers: [{ platform: "zone", ...HaZoneTrigger.defaultConfig }], triggers: [{ ...HaZoneTrigger.defaultConfig }],
}, },
{ {
name: "Tag", name: "Tag",
triggers: [{ platform: "tag", ...HaTagTrigger.defaultConfig }], triggers: [{ ...HaTagTrigger.defaultConfig }],
}, },
{ {
name: "Time", name: "Time",
triggers: [{ platform: "time", ...HaTimeTrigger.defaultConfig }], triggers: [{ ...HaTimeTrigger.defaultConfig }],
}, },
{ {
name: "Template", name: "Template",
triggers: [{ platform: "template", ...HaTemplateTrigger.defaultConfig }], triggers: [{ ...HaTemplateTrigger.defaultConfig }],
}, },
{ {
name: "Event", name: "Event",
triggers: [{ platform: "event", ...HaEventTrigger.defaultConfig }], triggers: [{ ...HaEventTrigger.defaultConfig }],
}, },
{ {
name: "Device Trigger", name: "Device Trigger",
triggers: [{ platform: "device", ...HaDeviceTrigger.defaultConfig }], triggers: [{ ...HaDeviceTrigger.defaultConfig }],
}, },
{ {
name: "Sentence", name: "Sentence",
triggers: [ triggers: [
{ platform: "conversation", ...HaConversationTrigger.defaultConfig }, { ...HaConversationTrigger.defaultConfig },
{ {
platform: "conversation", platform: "conversation",
command: ["Turn on the lights", "Turn the lights on"], command: ["Turn on the lights", "Turn the lights on"],

View File

@ -0,0 +1,3 @@
---
title: Markdown
---

View File

@ -0,0 +1,93 @@
import { css, html, LitElement } from "lit";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-markdown";
import { customElement } from "lit/decorators";
interface MarkdownContent {
content: string;
breaks: boolean;
allowSvg: boolean;
lazyImages: boolean;
}
const mdContentwithDefaults = (md: Partial<MarkdownContent>) =>
({
breaks: false,
allowSvg: false,
lazyImages: false,
...md,
}) as MarkdownContent;
const generateContent = (md) => `
\`\`\`json
${JSON.stringify({ ...md, content: undefined })}
\`\`\`
---
${md.content}
`;
const markdownContents: MarkdownContent[] = [
mdContentwithDefaults({
content: "_Hello_ **there** 👋, ~~nice~~ of you ||to|| show up.",
}),
...[true, false].map((breaks) =>
mdContentwithDefaults({
breaks,
content: `
![image](https://img.shields.io/badge/markdown-rendering-brightgreen)
![image](https://img.shields.io/badge/markdown-rendering-blue)
> [!TIP]
> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer dictum quis ante eu eleifend. Integer sed [consectetur est, nec elementum magna](#). Fusce lobortis lectus ac rutrum tincidunt. Quisque suscipit gravida ante, in convallis risus vulputate non.
key | description
-- | --
lorem | ipsum
- list item 1
- list item 2
`,
})
),
];
@customElement("demo-misc-ha-markdown")
export class DemoMiscMarkdown extends LitElement {
protected render() {
return html`
<div class="container">
${markdownContents.map(
(md) =>
html`<ha-card>
<ha-markdown
.content=${generateContent(md)}
.breaks=${md.breaks}
.allowSvg=${md.allowSvg}
.lazyImages=${md.lazyImages}
></ha-markdown>
</ha-card>`
)}
</div>
`;
}
static get styles() {
return css`
ha-card {
margin: 12px;
padding: 12px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-misc-ha-markdown": DemoMiscMarkdown;
}
}

View File

@ -25,7 +25,7 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@babel/runtime": "7.25.4", "@babel/runtime": "7.25.6",
"@braintree/sanitize-url": "7.1.0", "@braintree/sanitize-url": "7.1.0",
"@codemirror/autocomplete": "6.18.0", "@codemirror/autocomplete": "6.18.0",
"@codemirror/commands": "6.6.0", "@codemirror/commands": "6.6.0",
@ -33,7 +33,7 @@
"@codemirror/legacy-modes": "6.4.1", "@codemirror/legacy-modes": "6.4.1",
"@codemirror/search": "6.5.6", "@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1", "@codemirror/state": "6.4.1",
"@codemirror/view": "6.32.0", "@codemirror/view": "6.33.0",
"@egjs/hammerjs": "2.0.17", "@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.12.5", "@formatjs/intl-datetimeformat": "6.12.5",
"@formatjs/intl-displaynames": "6.6.8", "@formatjs/intl-displaynames": "6.6.8",
@ -88,8 +88,8 @@
"@polymer/paper-tabs": "3.1.0", "@polymer/paper-tabs": "3.1.0",
"@polymer/polymer": "3.5.1", "@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0", "@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.4.5", "@vaadin/combo-box": "24.4.7",
"@vaadin/vaadin-themable-mixin": "24.4.5", "@vaadin/vaadin-themable-mixin": "24.4.7",
"@vibrant/color": "3.2.1-alpha.1", "@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1", "@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@ -118,7 +118,7 @@
"leaflet-draw": "1.0.4", "leaflet-draw": "1.0.4",
"lit": "2.8.0", "lit": "2.8.0",
"luxon": "3.5.0", "luxon": "3.5.0",
"marked": "14.0.0", "marked": "14.1.0",
"memoize-one": "6.0.0", "memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1", "node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2", "proxy-polyfill": "0.3.2",
@ -155,7 +155,7 @@
"@babel/plugin-transform-runtime": "7.25.4", "@babel/plugin-transform-runtime": "7.25.4",
"@babel/preset-env": "7.25.4", "@babel/preset-env": "7.25.4",
"@babel/preset-typescript": "7.24.7", "@babel/preset-typescript": "7.24.7",
"@bundle-stats/plugin-webpack-filter": "4.14.2", "@bundle-stats/plugin-webpack-filter": "4.15.0",
"@koa/cors": "5.0.0", "@koa/cors": "5.0.0",
"@lokalise/node-api": "12.7.0", "@lokalise/node-api": "12.7.0",
"@octokit/auth-oauth-device": "7.1.1", "@octokit/auth-oauth-device": "7.1.1",
@ -258,5 +258,5 @@
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch", "sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch",
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch" "leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
}, },
"packageManager": "yarn@4.4.0" "packageManager": "yarn@4.4.1"
} }

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20240809.0" version = "20240903.1"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "The Home Assistant frontend" description = "The Home Assistant frontend"
readme = "README.md" readme = "README.md"

View File

@ -71,8 +71,7 @@ export const computeStateDisplayFromEntityAttributes = (
if ( if (
attributes.device_class === "duration" && attributes.device_class === "duration" &&
attributes.unit_of_measurement && attributes.unit_of_measurement &&
UNIT_TO_MILLISECOND_CONVERT[attributes.unit_of_measurement] && UNIT_TO_MILLISECOND_CONVERT[attributes.unit_of_measurement]
entity?.display_precision === undefined
) { ) {
try { try {
return formatDuration(state, attributes.unit_of_measurement); return formatDuration(state, attributes.unit_of_measurement);

View File

@ -0,0 +1,6 @@
import type { ChartEvent } from "chart.js";
export const clickIsTouch = (event: ChartEvent): boolean =>
!(event.native instanceof MouseEvent) ||
(event.native instanceof PointerEvent &&
event.native.pointerType !== "mouse");

View File

@ -16,6 +16,7 @@ import {
HaChartBase, HaChartBase,
MIN_TIME_BETWEEN_UPDATES, MIN_TIME_BETWEEN_UPDATES,
} from "./ha-chart-base"; } from "./ha-chart-base";
import { clickIsTouch } from "./click_is_touch";
const safeParseFloat = (value) => { const safeParseFloat = (value) => {
const parsed = parseFloat(value); const parsed = parseFloat(value);
@ -220,12 +221,7 @@ export class StateHistoryChartLine extends LitElement {
// @ts-expect-error // @ts-expect-error
locale: numberFormatToLocale(this.hass.locale), locale: numberFormatToLocale(this.hass.locale),
onClick: (e: any) => { onClick: (e: any) => {
if ( if (!this.clickForMoreInfo || clickIsTouch(e)) {
!this.clickForMoreInfo ||
!(e.native instanceof MouseEvent) ||
(e.native instanceof PointerEvent &&
e.native.pointerType !== "mouse")
) {
return; return;
} }

View File

@ -16,6 +16,7 @@ import {
} from "./ha-chart-base"; } from "./ha-chart-base";
import type { TimeLineData } from "./timeline-chart/const"; import type { TimeLineData } from "./timeline-chart/const";
import { computeTimelineColor } from "./timeline-chart/timeline-color"; import { computeTimelineColor } from "./timeline-chart/timeline-color";
import { clickIsTouch } from "./click_is_touch";
@customElement("state-history-chart-timeline") @customElement("state-history-chart-timeline")
export class StateHistoryChartTimeline extends LitElement { export class StateHistoryChartTimeline extends LitElement {
@ -224,11 +225,7 @@ export class StateHistoryChartTimeline extends LitElement {
// @ts-expect-error // @ts-expect-error
locale: numberFormatToLocale(this.hass.locale), locale: numberFormatToLocale(this.hass.locale),
onClick: (e: any) => { onClick: (e: any) => {
if ( if (!this.clickForMoreInfo || clickIsTouch(e)) {
!this.clickForMoreInfo ||
!(e.native instanceof MouseEvent) ||
(e.native instanceof PointerEvent && e.native.pointerType !== "mouse")
) {
return; return;
} }

View File

@ -39,6 +39,7 @@ import type {
ChartDatasetExtra, ChartDatasetExtra,
HaChartBase, HaChartBase,
} from "./ha-chart-base"; } from "./ha-chart-base";
import { clickIsTouch } from "./click_is_touch";
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = { export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
mean: "mean", mean: "mean",
@ -278,11 +279,7 @@ export class StatisticsChart extends LitElement {
// @ts-expect-error // @ts-expect-error
locale: numberFormatToLocale(this.hass.locale), locale: numberFormatToLocale(this.hass.locale),
onClick: (e: any) => { onClick: (e: any) => {
if ( if (!this.clickForMoreInfo || clickIsTouch(e)) {
!this.clickForMoreInfo ||
!(e.native instanceof MouseEvent) ||
(e.native instanceof PointerEvent && e.native.pointerType !== "mouse")
) {
return; return;
} }

View File

@ -45,15 +45,35 @@ export class HaConversationAgentPicker extends LitElement {
if (!this._agents) { if (!this._agents) {
return nothing; return nothing;
} }
const value = let value = this.value;
this.value ?? if (!value && this.required) {
(this.required && // Select Home Assistant conversation agent if it supports the language
(!this.language || for (const agent of this._agents) {
this._agents if (
.find((agent) => agent.id === "homeassistant") agent.id === "conversation.home_assistant" &&
?.supported_languages.includes(this.language)) agent.supported_languages.includes(this.language!)
? "homeassistant" ) {
: NONE); value = agent.id;
break;
}
}
if (!value) {
// Select the first agent that supports the language
for (const agent of this._agents) {
if (
agent.supported_languages === "*" &&
agent.supported_languages.includes(this.language!)
) {
value = agent.id;
break;
}
}
}
}
if (!value) {
value = NONE;
}
return html` return html`
<ha-select <ha-select
.label=${this.label || .label=${this.label ||

View File

@ -68,8 +68,8 @@ export class HaExpansionPanel extends LitElement {
></ha-svg-icon> ></ha-svg-icon>
` `
: ""} : ""}
<slot name="icons"></slot>
</div> </div>
<slot name="icons"></slot>
</div> </div>
<div <div
class="container ${classMap({ expanded: this.expanded })}" class="container ${classMap({ expanded: this.expanded })}"

View File

@ -21,13 +21,45 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
@property({ attribute: false }) public computeLabel?: ( @property({ attribute: false }) public computeLabel?: (
schema: HaFormSchema, schema: HaFormSchema,
data?: HaFormDataContainer data?: HaFormDataContainer,
options?: { path?: string[] }
) => string; ) => string;
@property({ attribute: false }) public computeHelper?: ( @property({ attribute: false }) public computeHelper?: (
schema: HaFormSchema schema: HaFormSchema,
options?: { path?: string[] }
) => string; ) => string;
private _renderDescription() {
const description = this.computeHelper?.(this.schema);
return description ? html`<p>${description}</p>` : nothing;
}
private _computeLabel = (
schema: HaFormSchema,
data?: HaFormDataContainer,
options?: { path?: string[] }
) => {
if (!this.computeLabel) return this.computeLabel;
return this.computeLabel(schema, data, {
...options,
path: [...(options?.path || []), this.schema.name],
});
};
private _computeHelper = (
schema: HaFormSchema,
options?: { path?: string[] }
) => {
if (!this.computeHelper) return this.computeHelper;
return this.computeHelper(schema, {
...options,
path: [...(options?.path || []), this.schema.name],
});
};
protected render() { protected render() {
return html` return html`
<ha-expansion-panel outlined .expanded=${Boolean(this.schema.expanded)}> <ha-expansion-panel outlined .expanded=${Boolean(this.schema.expanded)}>
@ -43,16 +75,17 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
<ha-svg-icon .path=${this.schema.iconPath}></ha-svg-icon> <ha-svg-icon .path=${this.schema.iconPath}></ha-svg-icon>
` `
: nothing} : nothing}
${this.schema.title} ${this.schema.title || this.computeLabel?.(this.schema)}
</div> </div>
<div class="content"> <div class="content">
${this._renderDescription()}
<ha-form <ha-form
.hass=${this.hass} .hass=${this.hass}
.data=${this.data} .data=${this.data}
.schema=${this.schema.schema} .schema=${this.schema.schema}
.disabled=${this.disabled} .disabled=${this.disabled}
.computeLabel=${this.computeLabel} .computeLabel=${this._computeLabel}
.computeHelper=${this.computeHelper} .computeHelper=${this._computeHelper}
></ha-form> ></ha-form>
</div> </div>
</ha-expansion-panel> </ha-expansion-panel>
@ -71,6 +104,9 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
.content { .content {
padding: 12px; padding: 12px;
} }
.content p {
margin: 0 0 24px;
}
ha-expansion-panel { ha-expansion-panel {
display: block; display: block;
--expansion-panel-content-padding: 0; --expansion-panel-content-padding: 0;

View File

@ -31,7 +31,7 @@ const LOAD_ELEMENTS = {
}; };
const getValue = (obj, item) => const getValue = (obj, item) =>
obj ? (!item.name ? obj : obj[item.name]) : null; obj ? (!item.name || item.flatten ? obj : obj[item.name]) : null;
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null); const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
@ -73,10 +73,6 @@ export class HaForm extends LitElement implements HaFormElement {
schema: any schema: any
) => string | undefined; ) => string | undefined;
@property({ attribute: false }) public localizeValue?: (
key: string
) => string;
protected getFormProperties(): Record<string, any> { protected getFormProperties(): Record<string, any> {
return {}; return {};
} }
@ -149,7 +145,6 @@ export class HaForm extends LitElement implements HaFormElement {
.disabled=${item.disabled || this.disabled || false} .disabled=${item.disabled || this.disabled || false}
.placeholder=${item.required ? "" : item.default} .placeholder=${item.required ? "" : item.default}
.helper=${this._computeHelper(item)} .helper=${this._computeHelper(item)}
.localizeValue=${this.localizeValue}
.required=${item.required || false} .required=${item.required || false}
.context=${this._generateContext(item)} .context=${this._generateContext(item)}
></ha-selector>` ></ha-selector>`
@ -204,9 +199,10 @@ export class HaForm extends LitElement implements HaFormElement {
if (ev.target === this) return; if (ev.target === this) return;
const newValue = !schema.name const newValue =
? ev.detail.value !schema.name || ("flatten" in schema && schema.flatten)
: { [schema.name]: ev.detail.value }; ? ev.detail.value
: { [schema.name]: ev.detail.value };
this.data = { this.data = {
...this.data, ...this.data,

View File

@ -31,15 +31,15 @@ export interface HaFormBaseSchema {
export interface HaFormGridSchema extends HaFormBaseSchema { export interface HaFormGridSchema extends HaFormBaseSchema {
type: "grid"; type: "grid";
name: string; flatten?: boolean;
column_min_width?: string; column_min_width?: string;
schema: readonly HaFormSchema[]; schema: readonly HaFormSchema[];
} }
export interface HaFormExpandableSchema extends HaFormBaseSchema { export interface HaFormExpandableSchema extends HaFormBaseSchema {
type: "expandable"; type: "expandable";
name: ""; flatten?: boolean;
title: string; title?: string;
icon?: string; icon?: string;
iconPath?: string; iconPath?: string;
expanded?: boolean; expanded?: boolean;
@ -100,7 +100,7 @@ export type SchemaUnion<
SchemaArray extends readonly HaFormSchema[], SchemaArray extends readonly HaFormSchema[],
Schema = SchemaArray[number], Schema = SchemaArray[number],
> = Schema extends HaFormGridSchema | HaFormExpandableSchema > = Schema extends HaFormGridSchema | HaFormExpandableSchema
? SchemaUnion<Schema["schema"]> ? SchemaUnion<Schema["schema"]> | Schema
: Schema; : Schema;
export interface HaFormDataContainer { export interface HaFormDataContainer {

View File

@ -18,9 +18,9 @@ export class HaFormfield extends FormfieldBase {
return html` <div class="mdc-form-field ${classMap(classes)}"> return html` <div class="mdc-form-field ${classMap(classes)}">
<slot></slot> <slot></slot>
<label class="mdc-label" @click=${this._labelClick} <label class="mdc-label" @click=${this._labelClick}>
><slot name="label">${this.label}</slot></label <slot name="label">${this.label}</slot>
> </label>
</div>`; </div>`;
} }
@ -57,13 +57,13 @@ export class HaFormfield extends FormfieldBase {
} }
.mdc-form-field { .mdc-form-field {
align-items: var(--ha-formfield-align-items, center); align-items: var(--ha-formfield-align-items, center);
gap: 4px;
} }
.mdc-form-field > label { .mdc-form-field > label {
direction: var(--direction); direction: var(--direction);
margin-inline-start: 0; margin-inline-start: 0;
margin-inline-end: auto; margin-inline-end: auto;
padding-inline-start: 4px; padding: 0;
padding-inline-end: 0;
} }
:host([disabled]) label { :host([disabled]) label {
color: var(--disabled-text-color); color: var(--disabled-text-color);

View File

@ -1,24 +1,24 @@
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "./ha-icon-button";
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider"; import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
import "./ha-icon-button";
import { mdiRestore } from "@mdi/js"; import { mdiRestore } from "@mdi/js";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types";
import { conditionalClamp } from "../common/number/clamp"; import { conditionalClamp } from "../common/number/clamp";
import {
type GridSizeValue = { CardGridSize,
rows?: number | "auto"; DEFAULT_GRID_SIZE,
columns?: number; } from "../panels/lovelace/common/compute-card-grid-size";
}; import { HomeAssistant } from "../types";
@customElement("ha-grid-size-picker") @customElement("ha-grid-size-picker")
export class HaGridSizeEditor extends LitElement { export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: GridSizeValue; @property({ attribute: false }) public value?: CardGridSize;
@property({ attribute: false }) public rows = 8; @property({ attribute: false }) public rows = 8;
@ -34,7 +34,7 @@ export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public isDefault?: boolean; @property({ attribute: false }) public isDefault?: boolean;
@state() public _localValue?: GridSizeValue = undefined; @state() public _localValue?: CardGridSize = { rows: 1, columns: 1 };
protected willUpdate(changedProperties) { protected willUpdate(changedProperties) {
if (changedProperties.has("value")) { if (changedProperties.has("value")) {
@ -49,6 +49,7 @@ export class HaGridSizeEditor extends LitElement {
this.rowMin !== undefined && this.rowMin === this.rowMax; this.rowMin !== undefined && this.rowMin === this.rowMax;
const autoHeight = this._localValue?.rows === "auto"; const autoHeight = this._localValue?.rows === "auto";
const fullWidth = this._localValue?.columns === "full";
const rowMin = this.rowMin ?? 1; const rowMin = this.rowMin ?? 1;
const rowMax = this.rowMax ?? this.rows; const rowMax = this.rowMax ?? this.rows;
@ -67,7 +68,7 @@ export class HaGridSizeEditor extends LitElement {
.min=${columnMin} .min=${columnMin}
.max=${columnMax} .max=${columnMax}
.range=${this.columns} .range=${this.columns}
.value=${columnValue} .value=${fullWidth ? this.columns : columnValue}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved} @slider-moved=${this._sliderMoved}
.disabled=${disabledColumns} .disabled=${disabledColumns}
@ -104,12 +105,12 @@ export class HaGridSizeEditor extends LitElement {
` `
: nothing} : nothing}
<div <div
class="preview" class="preview ${classMap({ "full-width": fullWidth })}"
style=${styleMap({ style=${styleMap({
"--total-rows": this.rows, "--total-rows": this.rows,
"--total-columns": this.columns, "--total-columns": this.columns,
"--rows": rowValue, "--rows": rowValue,
"--columns": columnValue, "--columns": fullWidth ? this.columns : columnValue,
})} })}
> >
<div> <div>
@ -140,12 +141,21 @@ export class HaGridSizeEditor extends LitElement {
const cell = ev.currentTarget as HTMLElement; const cell = ev.currentTarget as HTMLElement;
const rows = Number(cell.getAttribute("data-row")); const rows = Number(cell.getAttribute("data-row"));
const columns = Number(cell.getAttribute("data-column")); const columns = Number(cell.getAttribute("data-column"));
const clampedRow = conditionalClamp(rows, this.rowMin, this.rowMax); const clampedRow: CardGridSize["rows"] = conditionalClamp(
const clampedColumn = conditionalClamp( rows,
this.rowMin,
this.rowMax
);
let clampedColumn: CardGridSize["columns"] = conditionalClamp(
columns, columns,
this.columnMin, this.columnMin,
this.columnMax this.columnMax
); );
const currentSize = this.value ?? DEFAULT_GRID_SIZE;
if (currentSize.columns === "full" && clampedColumn === this.columns) {
clampedColumn = "full";
}
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { rows: clampedRow, columns: clampedColumn }, value: { rows: clampedRow, columns: clampedColumn },
}); });
@ -153,12 +163,23 @@ export class HaGridSizeEditor extends LitElement {
private _valueChanged(ev) { private _valueChanged(ev) {
ev.stopPropagation(); ev.stopPropagation();
const key = ev.currentTarget.id; const key = ev.currentTarget.id as "rows" | "columns";
const newValue = { const currentSize = this.value ?? DEFAULT_GRID_SIZE;
...this.value, let value = ev.detail.value as CardGridSize[typeof key];
[key]: ev.detail.value,
if (
key === "columns" &&
currentSize.columns === "full" &&
value === this.columns
) {
value = "full";
}
const newSize = {
...currentSize,
[key]: value,
}; };
fireEvent(this, "value-changed", { value: newValue }); fireEvent(this, "value-changed", { value: newSize });
} }
private _reset(ev) { private _reset(ev) {
@ -173,11 +194,14 @@ export class HaGridSizeEditor extends LitElement {
private _sliderMoved(ev) { private _sliderMoved(ev) {
ev.stopPropagation(); ev.stopPropagation();
const key = ev.currentTarget.id; const key = ev.currentTarget.id as "rows" | "columns";
const value = ev.detail.value; const currentSize = this.value ?? DEFAULT_GRID_SIZE;
const value = ev.detail.value as CardGridSize[typeof key] | undefined;
if (value === undefined) return; if (value === undefined) return;
this._localValue = { this._localValue = {
...this.value, ...currentSize,
[key]: ev.detail.value, [key]: ev.detail.value,
}; };
} }
@ -189,7 +213,7 @@ export class HaGridSizeEditor extends LitElement {
grid-template-areas: grid-template-areas:
"reset column-slider" "reset column-slider"
"row-slider preview"; "row-slider preview";
grid-template-rows: auto 1fr; grid-template-rows: auto auto;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
gap: 8px; gap: 8px;
} }
@ -205,17 +229,12 @@ export class HaGridSizeEditor extends LitElement {
.preview { .preview {
position: relative; position: relative;
grid-area: preview; grid-area: preview;
aspect-ratio: 1 / 1.2;
} }
.preview > div { .preview > div {
position: absolute; position: relative;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: grid; display: grid;
grid-template-columns: repeat(var(--total-columns), 1fr); grid-template-columns: repeat(var(--total-columns), 1fr);
grid-template-rows: repeat(var(--total-rows), 1fr); grid-template-rows: repeat(var(--total-rows), 25px);
gap: 4px; gap: 4px;
} }
.preview .cell { .preview .cell {
@ -226,15 +245,23 @@ export class HaGridSizeEditor extends LitElement {
opacity: 0.2; opacity: 0.2;
cursor: pointer; cursor: pointer;
} }
.selected { .preview .selected {
position: absolute;
pointer-events: none; pointer-events: none;
top: 0;
left: 0;
height: 100%;
width: 100%;
} }
.selected .cell { .selected .cell {
background-color: var(--primary-color); background-color: var(--primary-color);
grid-column: 1 / span var(--columns, 0); grid-column: 1 / span min(var(--columns, 0), var(--total-columns));
grid-row: 1 / span var(--rows, 0); grid-row: 1 / span min(var(--rows, 0), var(--total-rows));
opacity: 0.5; opacity: 0.5;
} }
.preview.full-width .selected .cell {
grid-column: 1 / -1;
}
`, `,
]; ];
} }

View File

@ -96,7 +96,25 @@ class HaMarkdownElement extends ReactiveElement {
haAlertNode.append( haAlertNode.append(
...Array.from(node.childNodes) ...Array.from(node.childNodes)
.map((child) => Array.from(child.childNodes)) .map((child) => {
const arr = Array.from(child.childNodes);
if (!this.breaks && arr.length) {
// When we are not breaking, the first line of the blockquote is not considered,
// so we need to adjust the first child text content
const firstChild = arr[0];
if (
firstChild.nodeType === Node.TEXT_NODE &&
firstChild.textContent === gitHubAlertMatch.input &&
firstChild.textContent?.includes("\n")
) {
firstChild.textContent = firstChild.textContent
.split("\n")
.slice(1)
.join("\n");
}
}
return arr;
})
.reduce((acc, val) => acc.concat(val), []) .reduce((acc, val) => acc.concat(val), [])
.filter( .filter(
(childNode) => (childNode) =>

View File

@ -1,4 +1,4 @@
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
@ -28,10 +28,13 @@ export class HaBooleanSelector extends LitElement {
@change=${this._handleChange} @change=${this._handleChange}
.disabled=${this.disabled} .disabled=${this.disabled}
></ha-switch> ></ha-switch>
<span slot="label">
<p class="primary">${this.label}</p>
${this.helper
? html`<p class="secondary">${this.helper}</p>`
: nothing}
</span>
</ha-formfield> </ha-formfield>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
`; `;
} }
@ -47,10 +50,21 @@ export class HaBooleanSelector extends LitElement {
return css` return css`
ha-formfield { ha-formfield {
display: flex; display: flex;
height: 56px; min-height: 56px;
align-items: center; align-items: center;
--mdc-typography-body2-font-size: 1em; --mdc-typography-body2-font-size: 1em;
} }
p {
margin: 0;
}
.secondary {
direction: var(--direction);
padding-top: 4px;
box-sizing: border-box;
color: var(--secondary-text-color);
font-size: 0.875rem;
font-weight: var(--mdc-typography-body2-font-weight, 400);
}
`; `;
} }
} }

View File

@ -162,8 +162,14 @@ export class HaLocationSelector extends LitElement {
private _computeLabel = ( private _computeLabel = (
entry: SchemaUnion<ReturnType<typeof this._schema>> entry: SchemaUnion<ReturnType<typeof this._schema>>
): string => ): string => {
this.hass.localize(`ui.components.selectors.location.${entry.name}`); if (entry.name) {
return this.hass.localize(
`ui.components.selectors.location.${entry.name}`
);
}
return "";
};
static styles = css` static styles = css`
ha-locations-editor { ha-locations-editor {

View File

@ -1,4 +1,11 @@
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
@ -60,12 +67,10 @@ export class HaNumberSelector extends LitElement {
} }
return html` return html`
${this.label ? html`${this.label}${this.required ? "*" : ""}` : nothing}
<div class="input"> <div class="input">
${!isBox ${!isBox
? html` ? html`
${this.label
? html`${this.label}${this.required ? "*" : ""}`
: ""}
<ha-slider <ha-slider
labeled labeled
.min=${this.selector.number!.min} .min=${this.selector.number!.min}
@ -75,10 +80,11 @@ export class HaNumberSelector extends LitElement {
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required} .required=${this.required}
@change=${this._handleSliderChange} @change=${this._handleSliderChange}
.ticks=${this.selector.number?.slider_ticks}
> >
</ha-slider> </ha-slider>
` `
: ""} : nothing}
<ha-textfield <ha-textfield
.inputMode=${this.selector.number?.step === "any" || .inputMode=${this.selector.number?.step === "any" ||
(this.selector.number?.step ?? 1) % 1 !== 0 (this.selector.number?.step ?? 1) % 1 !== 0
@ -105,7 +111,7 @@ export class HaNumberSelector extends LitElement {
</div> </div>
${!isBox && this.helper ${!isBox && this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>` ? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""} : nothing}
`; `;
} }
@ -141,6 +147,9 @@ export class HaNumberSelector extends LitElement {
} }
ha-slider { ha-slider {
flex: 1; flex: 1;
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: 0;
} }
ha-textfield { ha-textfield {
--ha-textfield-input-width: 40px; --ha-textfield-input-width: 40px;

View File

@ -82,6 +82,7 @@ export class HaTextSelector extends LitElement {
.disabled=${this.disabled} .disabled=${this.disabled}
.type=${this._unmaskedPassword ? "text" : this.selector.text?.type} .type=${this._unmaskedPassword ? "text" : this.selector.text?.type}
@input=${this._handleChange} @input=${this._handleChange}
@change=${this._handleChange}
.label=${this.label || ""} .label=${this.label || ""}
.prefix=${this.selector.text?.prefix} .prefix=${this.selector.text?.prefix}
.suffix=${this.selector.text?.type === "password" .suffix=${this.selector.text?.type === "password"

View File

@ -30,7 +30,7 @@ export class HaTimeSelector extends LitElement {
clearable clearable
.helper=${this.helper} .helper=${this.helper}
.label=${this.label} .label=${this.label}
enable-second .enableSecond=${!this.selector.time?.no_second}
></ha-time-input> ></ha-time-input>
`; `;
} }

View File

@ -44,6 +44,7 @@ import "./ha-service-picker";
import "./ha-settings-row"; import "./ha-settings-row";
import "./ha-yaml-editor"; import "./ha-yaml-editor";
import type { HaYamlEditor } from "./ha-yaml-editor"; import type { HaYamlEditor } from "./ha-yaml-editor";
import "./ha-service-section-icon";
const attributeFilter = (values: any[], attribute: any) => { const attributeFilter = (values: any[], attribute: any) => {
if (typeof attribute === "object") { if (typeof attribute === "object") {
@ -496,7 +497,18 @@ export class HaServiceControl extends LitElement {
) || ) ||
dataField.name || dataField.name ||
dataField.key} dataField.key}
.secondary=${this._getSectionDescription(
dataField,
domain,
serviceName
)}
> >
<ha-service-section-icon
slot="icons"
.hass=${this.hass}
.service=${this._value!.action}
.section=${dataField.key}
></ha-service-section-icon>
${Object.entries(dataField.fields).map(([key, field]) => ${Object.entries(dataField.fields).map(([key, field]) =>
this._renderField( this._renderField(
{ key, ...field }, { key, ...field },
@ -517,6 +529,16 @@ export class HaServiceControl extends LitElement {
)} `; )} `;
} }
private _getSectionDescription(
dataField: ExtHassService["fields"][number],
domain: string | undefined,
serviceName: string | undefined
) {
return this.hass!.localize(
`component.${domain}.services.${serviceName}.sections.${dataField.key}.description`
);
}
private _renderField = ( private _renderField = (
dataField: ExtHassService["fields"][number], dataField: ExtHassService["fields"][number],
hasOptional: boolean, hasOptional: boolean,

View File

@ -0,0 +1,53 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-svg-icon";
import { serviceSectionIcon } from "../data/icons";
@customElement("ha-service-section-icon")
export class HaServiceSectionIcon extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public service?: string;
@property() public section?: string;
@property() public icon?: string;
protected render() {
if (this.icon) {
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
}
if (!this.service || !this.section) {
return nothing;
}
if (!this.hass) {
return this._renderFallback();
}
const icon = serviceSectionIcon(this.hass, this.service, this.section).then(
(icn) => {
if (icn) {
return html`<ha-icon .icon=${icn}></ha-icon>`;
}
return this._renderFallback();
}
);
return html`${until(icon)}`;
}
private _renderFallback() {
return nothing;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-service-section-icon": HaServiceSectionIcon;
}
}

View File

@ -20,6 +20,7 @@ export class HaSlider extends MdSlider {
--md-sys-color-on-surface: var(--primary-text-color); --md-sys-color-on-surface: var(--primary-text-color);
--md-slider-handle-width: 14px; --md-slider-handle-width: 14px;
--md-slider-handle-height: 14px; --md-slider-handle-height: 14px;
--md-slider-state-layer-size: 24px;
min-width: 100px; min-width: 100px;
min-inline-size: 100px; min-inline-size: 100px;
width: 200px; width: 200px;

View File

@ -16,11 +16,10 @@ import { HomeAssistant } from "../types";
import "./ha-list-item"; import "./ha-list-item";
import "./ha-select"; import "./ha-select";
import type { HaSelect } from "./ha-select"; import type { HaSelect } from "./ha-select";
import { computeDomain } from "../common/entity/compute_domain";
const NONE = "__NONE_OPTION__"; const NONE = "__NONE_OPTION__";
const NAME_MAP = { cloud: "Home Assistant Cloud" };
@customElement("ha-stt-picker") @customElement("ha-stt-picker")
export class HaSTTPicker extends LitElement { export class HaSTTPicker extends LitElement {
@property() public value?: string; @property() public value?: string;
@ -41,13 +40,32 @@ export class HaSTTPicker extends LitElement {
if (!this._engines) { if (!this._engines) {
return nothing; return nothing;
} }
const value =
this.value ?? let value = this.value;
(this.required if (!value && this.required) {
? this._engines.find( for (const entity of Object.values(this.hass.entities)) {
(engine) => engine.supported_languages?.length !== 0 if (
) entity.platform === "cloud" &&
: NONE); computeDomain(entity.entity_id) === "stt"
) {
value = entity.entity_id;
break;
}
}
if (!value) {
for (const sttEngine of this._engines) {
if (sttEngine?.supported_languages?.length !== 0) {
value = sttEngine.engine_id;
break;
}
}
}
}
if (!value) {
value = NONE;
}
return html` return html`
<ha-select <ha-select
.label=${this.label || .label=${this.label ||
@ -66,12 +84,15 @@ export class HaSTTPicker extends LitElement {
</ha-list-item>` </ha-list-item>`
: nothing} : nothing}
${this._engines.map((engine) => { ${this._engines.map((engine) => {
let label = engine.engine_id; if (engine.deprecated && engine.engine_id !== value) {
return nothing;
}
let label: string;
if (engine.engine_id.includes(".")) { if (engine.engine_id.includes(".")) {
const stateObj = this.hass!.states[engine.engine_id]; const stateObj = this.hass!.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id; label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else if (engine.engine_id in NAME_MAP) { } else {
label = NAME_MAP[engine.engine_id]; label = engine.name || engine.engine_id;
} }
return html`<ha-list-item return html`<ha-list-item
.value=${engine.engine_id} .value=${engine.engine_id}

View File

@ -16,14 +16,10 @@ import { HomeAssistant } from "../types";
import "./ha-list-item"; import "./ha-list-item";
import "./ha-select"; import "./ha-select";
import type { HaSelect } from "./ha-select"; import type { HaSelect } from "./ha-select";
import { computeDomain } from "../common/entity/compute_domain";
const NONE = "__NONE_OPTION__"; const NONE = "__NONE_OPTION__";
const NAME_MAP = {
cloud: "Home Assistant Cloud",
google_translate: "Google Translate",
};
@customElement("ha-tts-picker") @customElement("ha-tts-picker")
export class HaTTSPicker extends LitElement { export class HaTTSPicker extends LitElement {
@property() public value?: string; @property() public value?: string;
@ -44,13 +40,32 @@ export class HaTTSPicker extends LitElement {
if (!this._engines) { if (!this._engines) {
return nothing; return nothing;
} }
const value =
this.value ?? let value = this.value;
(this.required if (!value && this.required) {
? this._engines.find( for (const entity of Object.values(this.hass.entities)) {
(engine) => engine.supported_languages?.length !== 0 if (
) entity.platform === "cloud" &&
: NONE); computeDomain(entity.entity_id) === "tts"
) {
value = entity.entity_id;
break;
}
}
if (!value) {
for (const ttsEngine of this._engines) {
if (ttsEngine?.supported_languages?.length !== 0) {
value = ttsEngine.engine_id;
break;
}
}
}
}
if (!value) {
value = NONE;
}
return html` return html`
<ha-select <ha-select
.label=${this.label || .label=${this.label ||
@ -69,12 +84,15 @@ export class HaTTSPicker extends LitElement {
</ha-list-item>` </ha-list-item>`
: nothing} : nothing}
${this._engines.map((engine) => { ${this._engines.map((engine) => {
let label = engine.engine_id; if (engine.deprecated && engine.engine_id !== value) {
return nothing;
}
let label: string;
if (engine.engine_id.includes(".")) { if (engine.engine_id.includes(".")) {
const stateObj = this.hass!.states[engine.engine_id]; const stateObj = this.hass!.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id; label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else if (engine.engine_id in NAME_MAP) { } else {
label = NAME_MAP[engine.engine_id]; label = engine.name || engine.engine_id;
} }
return html`<ha-list-item return html`<ha-list-item
.value=${engine.engine_id} .value=${engine.engine_id}

View File

@ -11,6 +11,7 @@ import {
isLastDayOfMonth, isLastDayOfMonth,
} from "date-fns"; } from "date-fns";
import { Collection, getCollection } from "home-assistant-js-websocket"; import { Collection, getCollection } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import { import {
calcDate, calcDate,
calcDateProperty, calcDateProperty,
@ -791,3 +792,147 @@ export const getEnergyWaterUnit = (hass: HomeAssistant): string =>
export const energyStatisticHelpUrl = export const energyStatisticHelpUrl =
"/docs/energy/faq/#troubleshooting-missing-entities"; "/docs/energy/faq/#troubleshooting-missing-entities";
interface EnergySumData {
to_grid?: { [start: number]: number };
from_grid?: { [start: number]: number };
to_battery?: { [start: number]: number };
from_battery?: { [start: number]: number };
solar?: { [start: number]: number };
}
interface EnergyConsumptionData {
total: { [start: number]: number };
}
export const getSummedData = memoizeOne(
(
data: EnergyData
): { summedData: EnergySumData; compareSummedData?: EnergySumData } => {
const summedData = getSummedDataPartial(data);
const compareSummedData = data.statsCompare
? getSummedDataPartial(data, true)
: undefined;
return { summedData, compareSummedData };
}
);
const getSummedDataPartial = (
data: EnergyData,
compare?: boolean
): EnergySumData => {
const statIds: {
to_grid?: string[];
from_grid?: string[];
solar?: string[];
to_battery?: string[];
from_battery?: string[];
} = {};
for (const source of data.prefs.energy_sources) {
if (source.type === "solar") {
if (statIds.solar) {
statIds.solar.push(source.stat_energy_from);
} else {
statIds.solar = [source.stat_energy_from];
}
continue;
}
if (source.type === "battery") {
if (statIds.to_battery) {
statIds.to_battery.push(source.stat_energy_to);
statIds.from_battery!.push(source.stat_energy_from);
} else {
statIds.to_battery = [source.stat_energy_to];
statIds.from_battery = [source.stat_energy_from];
}
continue;
}
if (source.type !== "grid") {
continue;
}
// grid source
for (const flowFrom of source.flow_from) {
if (statIds.from_grid) {
statIds.from_grid.push(flowFrom.stat_energy_from);
} else {
statIds.from_grid = [flowFrom.stat_energy_from];
}
}
for (const flowTo of source.flow_to) {
if (statIds.to_grid) {
statIds.to_grid.push(flowTo.stat_energy_to);
} else {
statIds.to_grid = [flowTo.stat_energy_to];
}
}
}
const summedData: EnergySumData = {};
Object.entries(statIds).forEach(([key, subStatIds]) => {
const totalStats: { [start: number]: number } = {};
const sets: { [statId: string]: { [start: number]: number } } = {};
subStatIds!.forEach((id) => {
const stats = compare ? data.statsCompare[id] : data.stats[id];
if (!stats) {
return;
}
const set = {};
stats.forEach((stat) => {
if (stat.change === null || stat.change === undefined) {
return;
}
const val = stat.change;
// Get total of solar and to grid to calculate the solar energy used
totalStats[stat.start] =
stat.start in totalStats ? totalStats[stat.start] + val : val;
});
sets[id] = set;
});
summedData[key] = totalStats;
});
return summedData;
};
export const computeConsumptionData = memoizeOne(
(
data: EnergySumData,
compareData?: EnergySumData
): {
consumption: EnergyConsumptionData;
compareConsumption?: EnergyConsumptionData;
} => {
const consumption = computeConsumptionDataPartial(data);
const compareConsumption = compareData
? computeConsumptionDataPartial(compareData)
: undefined;
return { consumption, compareConsumption };
}
);
const computeConsumptionDataPartial = (
data: EnergySumData
): EnergyConsumptionData => {
const outData: EnergyConsumptionData = { total: {} };
Object.keys(data).forEach((type) => {
Object.keys(data[type]).forEach((start) => {
if (outData.total[start] === undefined) {
const consumption =
(data.from_grid?.[start] || 0) +
(data.solar?.[start] || 0) +
(data.from_battery?.[start] || 0) -
(data.to_grid?.[start] || 0) -
(data.to_battery?.[start] || 0);
outData.total[start] = consumption;
}
});
});
return outData;
};

View File

@ -62,7 +62,7 @@ export interface ComponentIcons {
} }
interface ServiceIcons { interface ServiceIcons {
[service: string]: string; [service: string]: { service: string; sections?: { [name: string]: string } };
} }
export type IconCategory = "entity" | "entity_component" | "services"; export type IconCategory = "entity" | "entity_component" | "services";
@ -288,7 +288,8 @@ export const serviceIcon = async (
const serviceName = computeObjectId(service); const serviceName = computeObjectId(service);
const serviceIcons = await getServiceIcons(hass, domain); const serviceIcons = await getServiceIcons(hass, domain);
if (serviceIcons) { if (serviceIcons) {
icon = serviceIcons[serviceName] as string; const srvceIcon = serviceIcons[serviceName] as ServiceIcons[string];
icon = srvceIcon?.service;
} }
if (!icon) { if (!icon) {
icon = await domainIcon(hass, domain); icon = await domainIcon(hass, domain);
@ -296,6 +297,21 @@ export const serviceIcon = async (
return icon; return icon;
}; };
export const serviceSectionIcon = async (
hass: HomeAssistant,
service: string,
section: string
): Promise<string | undefined> => {
const domain = computeDomain(service);
const serviceName = computeObjectId(service);
const serviceIcons = await getServiceIcons(hass, domain);
if (serviceIcons) {
const srvceIcon = serviceIcons[serviceName] as ServiceIcons[string];
return srvceIcon?.sections?.[section];
}
return undefined;
};
export const domainIcon = async ( export const domainIcon = async (
hass: HomeAssistant, hass: HomeAssistant,
domain: string, domain: string,

View File

@ -13,7 +13,7 @@ export const ensureBadgeConfig = (
return { return {
type: "entity", type: "entity",
entity: config, entity: config,
display_type: "complete", show_name: true,
}; };
} }
if ("type" in config && config.type) { if ("type" in config && config.type) {

View File

@ -5,6 +5,8 @@ import type { LovelaceStrategyConfig } from "./strategy";
export interface LovelaceBaseSectionConfig { export interface LovelaceBaseSectionConfig {
title?: string; title?: string;
visibility?: Condition[]; visibility?: Condition[];
column_span?: number;
row_span?: number;
} }
export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig { export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {

View File

@ -22,7 +22,9 @@ export interface LovelaceBaseViewConfig {
visible?: boolean | ShowViewConfig[]; visible?: boolean | ShowViewConfig[];
subview?: boolean; subview?: boolean;
back_path?: string; back_path?: string;
max_columns?: number; // Only used for section view, it should move to a section view config type when the views will have dedicated editor. // Only used for section view, it should move to a section view config type when the views will have dedicated editor.
max_columns?: number;
dense_section_placement?: boolean;
} }
export interface LovelaceViewConfig extends LovelaceBaseViewConfig { export interface LovelaceViewConfig extends LovelaceBaseViewConfig {

View File

@ -127,6 +127,12 @@ const tryDescribeAction = <T extends ActionType>(
targets.push( targets.push(
computeEntityRegistryName(hass, entityReg) || targetThing computeEntityRegistryName(hass, entityReg) || targetThing
); );
} else if (targetThing === "all") {
targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_every_entity`
)
);
} else { } else {
targets.push( targets.push(
hass.localize( hass.localize(

View File

@ -323,6 +323,7 @@ export interface NumberSelector {
step?: number | "any"; step?: number | "any";
mode?: "box" | "slider"; mode?: "box" | "slider";
unit_of_measurement?: string; unit_of_measurement?: string;
slider_ticks?: boolean;
} | null; } | null;
} }
@ -427,8 +428,7 @@ export interface ThemeSelector {
theme: { include_default?: boolean } | null; theme: { include_default?: boolean } | null;
} }
export interface TimeSelector { export interface TimeSelector {
// eslint-disable-next-line @typescript-eslint/ban-types time: { no_second?: boolean } | null;
time: {} | null;
} }
export interface TriggerSelector { export interface TriggerSelector {

View File

@ -21,6 +21,8 @@ export interface SpeechMetadata {
export interface STTEngine { export interface STTEngine {
engine_id: string; engine_id: string;
supported_languages?: string[]; supported_languages?: string[];
name?: string;
deprecated: boolean;
} }
export const listSTTEngines = ( export const listSTTEngines = (

View File

@ -3,6 +3,8 @@ import { HomeAssistant } from "../types";
export interface TTSEngine { export interface TTSEngine {
engine_id: string; engine_id: string;
supported_languages?: string[]; supported_languages?: string[];
name?: string;
deprecated: boolean;
} }
export interface TTSVoice { export interface TTSVoice {

View File

@ -76,17 +76,36 @@ export const showConfigFlowDialog = (
: ""; : "";
}, },
renderShowFormStepFieldLabel(hass, step, field) { renderShowFormStepFieldLabel(hass, step, field, options) {
return hass.localize( if (field.type === "expandable") {
`component.${step.handler}.config.step.${step.step_id}.data.${field.name}` return hass.localize(
`component.${step.handler}.config.step.${step.step_id}.sections.${field.name}.name`
);
}
const prefix = options?.path?.[0] ? `sections.${options.path[0]}` : "";
return (
hass.localize(
`component.${step.handler}.config.step.${step.step_id}.${prefix}data.${field.name}`
) || field.name
); );
}, },
renderShowFormStepFieldHelper(hass, step, field) { renderShowFormStepFieldHelper(hass, step, field, options) {
if (field.type === "expandable") {
return hass.localize(
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.sections.${field.name}.description`
);
}
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
const description = hass.localize( const description = hass.localize(
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.data_description.${field.name}`, `component.${step.translation_domain || step.handler}.config.step.${step.step_id}.${prefix}data_description.${field.name}`,
step.description_placeholders step.description_placeholders
); );
return description return description
? html`<ha-markdown breaks .content=${description}></ha-markdown>` ? html`<ha-markdown breaks .content=${description}></ha-markdown>`
: ""; : "";

View File

@ -49,13 +49,15 @@ export interface FlowConfig {
renderShowFormStepFieldLabel( renderShowFormStepFieldLabel(
hass: HomeAssistant, hass: HomeAssistant,
step: DataEntryFlowStepForm, step: DataEntryFlowStepForm,
field: HaFormSchema field: HaFormSchema,
options: { path?: string[]; [key: string]: any }
): string; ): string;
renderShowFormStepFieldHelper( renderShowFormStepFieldHelper(
hass: HomeAssistant, hass: HomeAssistant,
step: DataEntryFlowStepForm, step: DataEntryFlowStepForm,
field: HaFormSchema field: HaFormSchema,
options: { path?: string[]; [key: string]: any }
): TemplateResult | string; ): TemplateResult | string;
renderShowFormStepFieldError( renderShowFormStepFieldError(

View File

@ -93,15 +93,33 @@ export const showOptionsFlowDialog = (
: ""; : "";
}, },
renderShowFormStepFieldLabel(hass, step, field) { renderShowFormStepFieldLabel(hass, step, field, options) {
return hass.localize( if (field.type === "expandable") {
`component.${configEntry.domain}.options.step.${step.step_id}.data.${field.name}` return hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.sections.${field.name}.name`
);
}
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
return (
hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.${prefix}data.${field.name}`
) || field.name
); );
}, },
renderShowFormStepFieldHelper(hass, step, field) { renderShowFormStepFieldHelper(hass, step, field, options) {
if (field.type === "expandable") {
return hass.localize(
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.sections.${field.name}.description`
);
}
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
const description = hass.localize( const description = hass.localize(
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.data_description.${field.name}`, `component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.${prefix}data_description.${field.name}`,
step.description_placeholders step.description_placeholders
); );
return description return description

View File

@ -225,11 +225,24 @@ class StepFlowForm extends LitElement {
this._stepData = ev.detail.value; this._stepData = ev.detail.value;
} }
private _labelCallback = (field: HaFormSchema): string => private _labelCallback = (field: HaFormSchema, _data, options): string =>
this.flowConfig.renderShowFormStepFieldLabel(this.hass, this.step, field); this.flowConfig.renderShowFormStepFieldLabel(
this.hass,
this.step,
field,
options
);
private _helperCallback = (field: HaFormSchema): string | TemplateResult => private _helperCallback = (
this.flowConfig.renderShowFormStepFieldHelper(this.hass, this.step, field); field: HaFormSchema,
options
): string | TemplateResult =>
this.flowConfig.renderShowFormStepFieldHelper(
this.hass,
this.step,
field,
options
);
private _errorCallback = (error: string) => private _errorCallback = (error: string) =>
this.flowConfig.renderShowFormStepFieldError(this.hass, this.step, error); this.flowConfig.renderShowFormStepFieldError(this.hass, this.step, error);

View File

@ -86,7 +86,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
this._unsubMql = undefined; this._unsubMql = undefined;
} }
public static get defaultConfig() { public static get defaultConfig(): ChooseAction {
return { choose: [{ conditions: [], sequence: [] }] }; return { choose: [{ conditions: [], sequence: [] }] };
} }

View File

@ -20,7 +20,7 @@ export class HaConditionAction extends LitElement implements ActionElement {
@property({ attribute: false }) public action!: Condition; @property({ attribute: false }) public action!: Condition;
public static get defaultConfig() { public static get defaultConfig(): Omit<Condition, "state" | "entity_id"> {
return { condition: "state" }; return { condition: "state" };
} }
@ -87,13 +87,12 @@ export class HaConditionAction extends LitElement implements ActionElement {
const elClass = customElements.get( const elClass = customElements.get(
`ha-automation-condition-${type}` `ha-automation-condition-${type}`
) as CustomElementConstructor & { ) as CustomElementConstructor & {
defaultConfig: Omit<Condition, "condition">; defaultConfig: Condition;
}; };
if (type !== this.action.condition) { if (type !== this.action.condition) {
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { value: {
condition: type,
...elClass.defaultConfig, ...elClass.defaultConfig,
}, },
}); });

View File

@ -19,7 +19,7 @@ export class HaDelayAction extends LitElement implements ActionElement {
@state() private _timeData?: HaDurationData; @state() private _timeData?: HaDurationData;
public static get defaultConfig() { public static get defaultConfig(): DelayAction {
return { delay: "" }; return { delay: "" };
} }

View File

@ -36,7 +36,7 @@ export class HaDeviceAction extends LitElement {
private _origAction?: DeviceAction; private _origAction?: DeviceAction;
public static get defaultConfig() { public static get defaultConfig(): DeviceAction {
return { return {
device_id: "", device_id: "",
domain: "", domain: "",

View File

@ -21,7 +21,7 @@ export class HaIfAction extends LitElement implements ActionElement {
@state() private _showElse = false; @state() private _showElse = false;
public static get defaultConfig() { public static get defaultConfig(): IfAction {
return { return {
if: [], if: [],
then: [], then: [],

View File

@ -18,7 +18,7 @@ export class HaParallelAction extends LitElement implements ActionElement {
@property({ attribute: false }) public action!: ParallelAction; @property({ attribute: false }) public action!: ParallelAction;
public static get defaultConfig() { public static get defaultConfig(): ParallelAction {
return { return {
parallel: [], parallel: [],
}; };

View File

@ -31,7 +31,7 @@ export class HaRepeatAction extends LitElement implements ActionElement {
@property({ type: Array }) public path?: ItemPath; @property({ type: Array }) public path?: ItemPath;
public static get defaultConfig() { public static get defaultConfig(): RepeatAction {
return { repeat: { count: 2, sequence: [] } }; return { repeat: { count: 2, sequence: [] } };
} }

View File

@ -19,7 +19,7 @@ export class HaSequenceAction extends LitElement implements ActionElement {
@property({ attribute: false }) public action!: SequenceAction; @property({ attribute: false }) public action!: SequenceAction;
public static get defaultConfig() { public static get defaultConfig(): SequenceAction {
return { return {
sequence: [], sequence: [],
}; };

View File

@ -52,7 +52,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
} }
); );
public static get defaultConfig() { public static get defaultConfig(): ServiceAction {
return { action: "", data: {} }; return { action: "", data: {} };
} }

View File

@ -25,7 +25,7 @@ export class HaSetConversationResponseAction
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
public static get defaultConfig() { public static get defaultConfig(): SetConversationResponseAction {
return { set_conversation_response: "" }; return { set_conversation_response: "" };
} }

View File

@ -14,7 +14,7 @@ export class HaStopAction extends LitElement implements ActionElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
public static get defaultConfig() { public static get defaultConfig(): StopAction {
return { stop: "" }; return { stop: "" };
} }

View File

@ -25,7 +25,7 @@ export class HaWaitForTriggerAction
@property({ attribute: false }) public path?: ItemPath; @property({ attribute: false }) public path?: ItemPath;
public static get defaultConfig() { public static get defaultConfig(): WaitForTriggerAction {
return { wait_for_trigger: [] }; return { wait_for_trigger: [] };
} }

View File

@ -34,7 +34,7 @@ export class HaWaitAction extends LitElement implements ActionElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
public static get defaultConfig() { public static get defaultConfig(): WaitAction {
return { wait_template: "", continue_on_timeout: true }; return { wait_template: "", continue_on_timeout: true };
} }

View File

@ -207,10 +207,9 @@ export default class HaAutomationCondition extends LitElement {
const elClass = customElements.get( const elClass = customElements.get(
`ha-automation-condition-${condition}` `ha-automation-condition-${condition}`
) as CustomElementConstructor & { ) as CustomElementConstructor & {
defaultConfig: Omit<Condition, "condition">; defaultConfig: Condition;
}; };
conditions = this.conditions.concat({ conditions = this.conditions.concat({
condition: condition as any,
...elClass.defaultConfig, ...elClass.defaultConfig,
}); });
} }

View File

@ -1,8 +1,16 @@
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { HaLogicalCondition } from "./ha-automation-condition-logical"; import { HaLogicalCondition } from "./ha-automation-condition-logical";
import { LogicalCondition } from "../../../../../data/automation";
@customElement("ha-automation-condition-and") @customElement("ha-automation-condition-and")
export class HaAndCondition extends HaLogicalCondition {} export class HaAndCondition extends HaLogicalCondition {
public static get defaultConfig(): LogicalCondition {
return {
condition: "and",
conditions: [],
};
}
}
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

View File

@ -36,8 +36,9 @@ export class HaDeviceCondition extends LitElement {
private _origCondition?: DeviceCondition; private _origCondition?: DeviceCondition;
public static get defaultConfig() { public static get defaultConfig(): DeviceCondition {
return { return {
condition: "device",
device_id: "", device_id: "",
domain: "", domain: "",
entity_id: "", entity_id: "",

View File

@ -7,7 +7,10 @@ import "../ha-automation-condition";
import type { ConditionElement } from "../ha-automation-condition-row"; import type { ConditionElement } from "../ha-automation-condition-row";
@customElement("ha-automation-condition-logical") @customElement("ha-automation-condition-logical")
export class HaLogicalCondition extends LitElement implements ConditionElement { export abstract class HaLogicalCondition
extends LitElement
implements ConditionElement
{
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public condition!: LogicalCondition; @property({ attribute: false }) public condition!: LogicalCondition;
@ -16,12 +19,6 @@ export class HaLogicalCondition extends LitElement implements ConditionElement {
@property({ attribute: false }) public path?: ItemPath; @property({ attribute: false }) public path?: ItemPath;
public static get defaultConfig() {
return {
conditions: [],
};
}
protected render() { protected render() {
return html` return html`
<ha-automation-condition <ha-automation-condition

View File

@ -1,8 +1,16 @@
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { HaLogicalCondition } from "./ha-automation-condition-logical"; import { HaLogicalCondition } from "./ha-automation-condition-logical";
import { LogicalCondition } from "../../../../../data/automation";
@customElement("ha-automation-condition-not") @customElement("ha-automation-condition-not")
export class HaNotCondition extends HaLogicalCondition {} export class HaNotCondition extends HaLogicalCondition {
public static get defaultConfig(): LogicalCondition {
return {
condition: "not",
conditions: [],
};
}
}
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

View File

@ -20,8 +20,9 @@ export default class HaNumericStateCondition extends LitElement {
@state() private _inputBelowIsEntity?: boolean; @state() private _inputBelowIsEntity?: boolean;
public static get defaultConfig() { public static get defaultConfig(): NumericStateCondition {
return { return {
condition: "numeric_state",
entity_id: "", entity_id: "",
}; };
} }

View File

@ -1,8 +1,16 @@
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { HaLogicalCondition } from "./ha-automation-condition-logical"; import { HaLogicalCondition } from "./ha-automation-condition-logical";
import { LogicalCondition } from "../../../../../data/automation";
@customElement("ha-automation-condition-or") @customElement("ha-automation-condition-or")
export class HaOrCondition extends HaLogicalCondition {} export class HaOrCondition extends HaLogicalCondition {
public static get defaultConfig(): LogicalCondition {
return {
condition: "or",
conditions: [],
};
}
}
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

View File

@ -86,8 +86,8 @@ export class HaStateCondition extends LitElement implements ConditionElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
public static get defaultConfig() { public static get defaultConfig(): StateCondition {
return { entity_id: "", state: "" }; return { condition: "state", entity_id: "", state: "" };
} }
public shouldUpdate(changedProperties: PropertyValues) { public shouldUpdate(changedProperties: PropertyValues) {

View File

@ -17,8 +17,8 @@ export class HaSunCondition extends LitElement implements ConditionElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
public static get defaultConfig() { public static get defaultConfig(): SunCondition {
return {}; return { condition: "sun" };
} }
private _schema = memoizeOne( private _schema = memoizeOne(

View File

@ -13,8 +13,8 @@ export class HaTemplateCondition extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
public static get defaultConfig() { public static get defaultConfig(): TemplateCondition {
return { value_template: "" }; return { condition: "template", value_template: "" };
} }
protected render() { protected render() {

View File

@ -25,8 +25,8 @@ export class HaTimeCondition extends LitElement implements ConditionElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
public static get defaultConfig() { public static get defaultConfig(): TimeCondition {
return {}; return { condition: "time" };
} }
private _schema = memoizeOne( private _schema = memoizeOne(

View File

@ -27,8 +27,9 @@ export class HaTriggerCondition extends LitElement {
private _unsub?: UnsubscribeFunc; private _unsub?: UnsubscribeFunc;
public static get defaultConfig() { public static get defaultConfig(): TriggerCondition {
return { return {
condition: "trigger",
id: "", id: "",
}; };
} }

View File

@ -21,8 +21,9 @@ export class HaZoneCondition extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
public static get defaultConfig() { public static get defaultConfig(): ZoneCondition {
return { return {
condition: "zone",
entity_id: "", entity_id: "",
zone: "", zone: "",
}; };

View File

@ -143,10 +143,9 @@ export default class HaAutomationTrigger extends LitElement {
const elClass = customElements.get( const elClass = customElements.get(
`ha-automation-trigger-${platform}` `ha-automation-trigger-${platform}`
) as CustomElementConstructor & { ) as CustomElementConstructor & {
defaultConfig: Omit<Trigger, "platform">; defaultConfig: Trigger;
}; };
triggers = this.triggers.concat({ triggers = this.triggers.concat({
platform: platform as any,
...elClass.defaultConfig, ...elClass.defaultConfig,
}); });
} }

View File

@ -69,10 +69,12 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
] as const ] as const
); );
public static get defaultConfig() { public static get defaultConfig(): CalendarTrigger {
return { return {
platform: "calendar",
entity_id: "",
event: "start" as CalendarTrigger["event"], event: "start" as CalendarTrigger["event"],
offset: 0, offset: "0",
}; };
} }

View File

@ -25,8 +25,8 @@ export class HaConversationTrigger
@query("#option_input", true) private _optionInput?: HaTextField; @query("#option_input", true) private _optionInput?: HaTextField;
public static get defaultConfig(): Omit<ConversationTrigger, "platform"> { public static get defaultConfig(): ConversationTrigger {
return { command: "" }; return { platform: "conversation", command: "" };
} }
protected render() { protected render() {

View File

@ -38,8 +38,9 @@ export class HaDeviceTrigger extends LitElement {
private _origTrigger?: DeviceTrigger; private _origTrigger?: DeviceTrigger;
public static get defaultConfig() { public static get defaultConfig(): DeviceTrigger {
return { return {
platform: "device",
device_id: "", device_id: "",
domain: "", domain: "",
entity_id: "", entity_id: "",

View File

@ -19,8 +19,8 @@ export class HaEventTrigger extends LitElement implements TriggerElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
public static get defaultConfig() { public static get defaultConfig(): EventTrigger {
return { event_type: "" }; return { platform: "event", event_type: "" };
} }
protected render() { protected render() {

View File

@ -43,8 +43,9 @@ export class HaGeolocationTrigger extends LitElement {
] as const ] as const
); );
public static get defaultConfig() { public static get defaultConfig(): GeoLocationTrigger {
return { return {
platform: "geo_location",
source: "", source: "",
zone: "", zone: "",
event: "enter" as GeoLocationTrigger["event"], event: "enter" as GeoLocationTrigger["event"],

View File

@ -41,8 +41,9 @@ export class HaHassTrigger extends LitElement {
] as const ] as const
); );
public static get defaultConfig() { public static get defaultConfig(): HassTrigger {
return { return {
platform: "homeassistant",
event: "start" as HassTrigger["event"], event: "start" as HassTrigger["event"],
}; };
} }

View File

@ -20,8 +20,8 @@ export class HaMQTTTrigger extends LitElement implements TriggerElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
public static get defaultConfig() { public static get defaultConfig(): MqttTrigger {
return { topic: "" }; return { platform: "mqtt", topic: "" };
} }
protected render() { protected render() {

View File

@ -237,8 +237,9 @@ export class HaNumericStateTrigger extends LitElement {
} }
} }
public static get defaultConfig() { public static get defaultConfig(): NumericStateTrigger {
return { return {
platform: "numeric_state",
entity_id: [], entity_id: [],
}; };
} }

View File

@ -70,8 +70,9 @@ export class HaPersistentNotificationTrigger
] as const ] as const
); );
public static get defaultConfig() { public static get defaultConfig(): PersistentNotificationTrigger {
return { return {
platform: "persistent_notification",
update_type: [...DEFAULT_UPDATE_TYPES], update_type: [...DEFAULT_UPDATE_TYPES],
notification_id: DEFAULT_NOTIFICATION_ID, notification_id: DEFAULT_NOTIFICATION_ID,
}; };

View File

@ -48,8 +48,8 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
public static get defaultConfig() { public static get defaultConfig(): StateTrigger {
return { entity_id: [] }; return { platform: "state", entity_id: [] };
} }
private _schema = memoizeOne( private _schema = memoizeOne(

View File

@ -43,8 +43,9 @@ export class HaSunTrigger extends LitElement implements TriggerElement {
] as const ] as const
); );
public static get defaultConfig() { public static get defaultConfig(): SunTrigger {
return { return {
platform: "sun",
event: "sunrise" as SunTrigger["event"], event: "sunrise" as SunTrigger["event"],
offset: 0, offset: 0,
}; };

View File

@ -19,8 +19,8 @@ export class HaTagTrigger extends LitElement implements TriggerElement {
@state() private _tags?: Tag[]; @state() private _tags?: Tag[];
public static get defaultConfig() { public static get defaultConfig(): TagTrigger {
return { tag_id: "" }; return { platform: "tag", tag_id: "" };
} }
protected firstUpdated(changedProperties: PropertyValues) { protected firstUpdated(changedProperties: PropertyValues) {

View File

@ -22,8 +22,8 @@ export class HaTemplateTrigger extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
public static get defaultConfig() { public static get defaultConfig(): TemplateTrigger {
return { value_template: "" }; return { platform: "template", value_template: "" };
} }
public willUpdate(changedProperties: PropertyValues) { public willUpdate(changedProperties: PropertyValues) {

View File

@ -19,8 +19,8 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
@state() private _inputMode?: boolean; @state() private _inputMode?: boolean;
public static get defaultConfig() { public static get defaultConfig(): TimeTrigger {
return { at: "" }; return { platform: "time", at: "" };
} }
private _schema = memoizeOne( private _schema = memoizeOne(

View File

@ -21,8 +21,8 @@ export class HaTimePatternTrigger extends LitElement implements TriggerElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
public static get defaultConfig() { public static get defaultConfig(): TimePatternTrigger {
return {}; return { platform: "time_pattern" };
} }
protected render() { protected render() {

View File

@ -36,8 +36,9 @@ export class HaWebhookTrigger extends LitElement {
private _unsub?: UnsubscribeFunc; private _unsub?: UnsubscribeFunc;
public static get defaultConfig() { public static get defaultConfig(): WebhookTrigger {
return { return {
platform: "webhook",
allowed_methods: [...DEFAULT_METHODS], allowed_methods: [...DEFAULT_METHODS],
local_only: true, local_only: true,
webhook_id: DEFAULT_WEBHOOK_ID, webhook_id: DEFAULT_WEBHOOK_ID,

View File

@ -23,8 +23,9 @@ export class HaZoneTrigger extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
public static get defaultConfig() { public static get defaultConfig(): ZoneTrigger {
return { return {
platform: "zone",
entity_id: "", entity_id: "",
zone: "", zone: "",
event: "enter" as ZoneTrigger["event"], event: "enter" as ZoneTrigger["event"],

View File

@ -0,0 +1,133 @@
import { CSSResultGroup, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { createCloseHeading } from "../../../../components/ha-dialog";
import "../../../../components/ha-form/ha-form";
import "../../../../components/ha-button";
import { haStyleDialog } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import {
ScheduleBlockInfo,
ScheduleBlockInfoDialogParams,
} from "./show-dialog-schedule-block-info";
import type { SchemaUnion } from "../../../../components/ha-form/types";
const SCHEMA = [
{
name: "from",
required: true,
selector: { time: { no_second: true } },
},
{
name: "to",
required: true,
selector: { time: { no_second: true } },
},
];
class DialogScheduleBlockInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _error?: Record<string, string>;
@state() private _data?: ScheduleBlockInfo;
@state() private _params?: ScheduleBlockInfoDialogParams;
public showDialog(params: ScheduleBlockInfoDialogParams): void {
this._params = params;
this._error = undefined;
this._data = params.block;
}
public closeDialog(): void {
this._params = undefined;
this._data = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._params || !this._data) {
return nothing;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass!.localize(
"ui.dialogs.helper_settings.schedule.edit_schedule_block"
)
)}
>
<div>
<ha-form
.hass=${this.hass}
.schema=${SCHEMA}
.data=${this._data}
.error=${this._error}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
</div>
<ha-button
slot="secondaryAction"
class="warning"
@click=${this._deleteBlock}
>
${this.hass!.localize("ui.common.delete")}
</ha-button>
<ha-button slot="primaryAction" @click=${this._updateBlock}>
${this.hass!.localize("ui.common.save")}
</ha-button>
</ha-dialog>
`;
}
private _valueChanged(ev: CustomEvent) {
this._error = undefined;
this._data = ev.detail.value;
}
private _updateBlock() {
try {
this._params!.updateBlock!(this._data!);
this.closeDialog();
} catch (err: any) {
this._error = { base: err ? err.message : "Unknown error" };
}
}
private _deleteBlock() {
try {
this._params!.deleteBlock!();
this.closeDialog();
} catch (err: any) {
this._error = { base: err ? err.message : "Unknown error" };
}
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
switch (schema.name) {
case "from":
return this.hass!.localize("ui.dialogs.helper_settings.schedule.start");
case "to":
return this.hass!.localize("ui.dialogs.helper_settings.schedule.end");
}
return "";
};
static get styles(): CSSResultGroup {
return [haStyleDialog];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-schedule-block-info": DialogScheduleBlockInfo;
}
}
customElements.define("dialog-schedule-block-info", DialogScheduleBlockInfo);

View File

@ -20,7 +20,7 @@ import "../../../../components/ha-icon-picker";
import "../../../../components/ha-textfield"; import "../../../../components/ha-textfield";
import { Schedule, ScheduleDay, weekdays } from "../../../../data/schedule"; import { Schedule, ScheduleDay, weekdays } from "../../../../data/schedule";
import { TimeZone } from "../../../../data/translation"; import { TimeZone } from "../../../../data/translation";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; import { showScheduleBlockInfoDialog } from "./show-dialog-schedule-block-info";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
@ -352,21 +352,34 @@ class HaScheduleForm extends LitElement {
} }
private async _handleEventClick(info: any) { private async _handleEventClick(info: any) {
if (
!(await showConfirmationDialog(this, {
title: this.hass.localize("ui.dialogs.helper_settings.schedule.delete"),
text: this.hass.localize(
"ui.dialogs.helper_settings.schedule.confirm_delete"
),
destructive: true,
confirmText: this.hass.localize("ui.common.delete"),
}))
) {
return;
}
const [day, index] = info.event.id.split("-"); const [day, index] = info.event.id.split("-");
const value = [...this[`_${day}`]]; const item = [...this[`_${day}`]][index];
showScheduleBlockInfoDialog(this, {
block: item,
updateBlock: (newBlock) => this._updateBlock(day, index, newBlock),
deleteBlock: () => this._deleteBlock(day, index),
});
}
private _updateBlock(day, index, newBlock) {
const [fromH, fromM, _fromS] = newBlock.from.split(":");
newBlock.from = `${fromH}:${fromM}`;
const [toH, toM, _toS] = newBlock.to.split(":");
newBlock.to = `${toH}:${toM}`;
if (Number(toH) === 0 && Number(toM) === 0) {
newBlock.to = "24:00";
}
const newValue = { ...this._item };
newValue[day] = [...this._item![day]];
newValue[day][index] = newBlock;
fireEvent(this, "value-changed", {
value: newValue,
});
}
private _deleteBlock(day, index) {
const value = [...this[`_${day}`]];
const newValue = { ...this._item }; const newValue = { ...this._item };
value.splice(parseInt(index), 1); value.splice(parseInt(index), 1);
newValue[day] = value; newValue[day] = value;

View File

@ -0,0 +1,26 @@
import { fireEvent } from "../../../../common/dom/fire_event";
export interface ScheduleBlockInfo {
from: string;
to: string;
}
export interface ScheduleBlockInfoDialogParams {
block: ScheduleBlockInfo;
updateBlock?: (update: ScheduleBlockInfo) => void;
deleteBlock?: () => void;
}
export const loadScheduleBlockInfoDialog = () =>
import("./dialog-schedule-block-info");
export const showScheduleBlockInfoDialog = (
element: HTMLElement,
params: ScheduleBlockInfoDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-schedule-block-info",
dialogImport: loadScheduleBlockInfoDialog,
dialogParams: params,
});
};

View File

@ -8,6 +8,7 @@ import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box"
import { createCloseHeading } from "../../../../../components/ha-dialog"; import { createCloseHeading } from "../../../../../components/ha-dialog";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import "../../../../../components/buttons/ha-progress-button"; import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button"; import "../../../../../components/ha-button";
import "../../../../../components/ha-select"; import "../../../../../components/ha-select";
import "../../../../../components/ha-list-item"; import "../../../../../components/ha-list-item";
@ -70,10 +71,22 @@ class DialogZHAChangeChannel extends LitElement implements HassDialog {
this.hass.localize("ui.panel.config.zha.change_channel_dialog.title") this.hass.localize("ui.panel.config.zha.change_channel_dialog.title")
)} )}
> >
<p> <ha-alert alert-type="warning">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.zha.change_channel_dialog.migration_warning" "ui.panel.config.zha.change_channel_dialog.migration_warning"
)} )}
</ha-alert>
<p>
${this.hass.localize(
"ui.panel.config.zha.change_channel_dialog.description"
)}
</p>
<p>
${this.hass.localize(
"ui.panel.config.zha.change_channel_dialog.smart_explanation"
)}
</p> </p>
<p> <p>
@ -90,7 +103,11 @@ class DialogZHAChangeChannel extends LitElement implements HassDialog {
${VALID_CHANNELS.map( ${VALID_CHANNELS.map(
(newChannel) => (newChannel) =>
html`<ha-list-item .value=${String(newChannel)} html`<ha-list-item .value=${String(newChannel)}
>${newChannel}</ha-list-item >${newChannel === "auto"
? this.hass.localize(
"ui.panel.config.zha.change_channel_dialog.channel_auto"
)
: newChannel}</ha-list-item
>` >`
)} )}
</ha-select> </ha-select>

View File

@ -96,20 +96,20 @@ export const showRepairsFlowDialog = (
: ""; : "";
}, },
renderShowFormStepFieldLabel(hass, step, field) { renderShowFormStepFieldLabel(hass, step, field, options) {
return hass.localize( return hass.localize(
`component.${issue.domain}.issues.${ `component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id issue.translation_key || issue.issue_id
}.fix_flow.step.${step.step_id}.data.${field.name}`, }.fix_flow.step.${step.step_id}.${options?.prefix ? `section.${options.prefix[0]}.` : ""}data.${field.name}`,
step.description_placeholders step.description_placeholders
); );
}, },
renderShowFormStepFieldHelper(hass, step, field) { renderShowFormStepFieldHelper(hass, step, field, options) {
const description = hass.localize( const description = hass.localize(
`component.${issue.domain}.issues.${ `component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id issue.translation_key || issue.issue_id
}.fix_flow.step.${step.step_id}.data_description.${field.name}`, }.fix_flow.step.${step.step_id}.${options?.prefix ? `section.${options.prefix[0]}.` : ""}data_description.${field.name}`,
step.description_placeholders step.description_placeholders
); );
return description return description

View File

@ -28,6 +28,7 @@ import "./assist-pipeline-detail/assist-pipeline-detail-tts";
import "./assist-pipeline-detail/assist-pipeline-detail-wakeword"; import "./assist-pipeline-detail/assist-pipeline-detail-wakeword";
import "./debug/assist-render-pipeline-events"; import "./debug/assist-render-pipeline-events";
import { VoiceAssistantPipelineDetailsDialogParams } from "./show-dialog-voice-assistant-pipeline-detail"; import { VoiceAssistantPipelineDetailsDialogParams } from "./show-dialog-voice-assistant-pipeline-detail";
import { computeDomain } from "../../../common/entity/compute_domain";
@customElement("dialog-voice-assistant-pipeline-detail") @customElement("dialog-voice-assistant-pipeline-detail")
export class DialogVoiceAssistantPipelineDetail extends LitElement { export class DialogVoiceAssistantPipelineDetail extends LitElement {
@ -54,15 +55,36 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement {
if (this._params.pipeline) { if (this._params.pipeline) {
this._data = this._params.pipeline; this._data = this._params.pipeline;
this._preferred = this._params.preferred; this._preferred = this._params.preferred;
} else { return;
this._data = {
language: (
this.hass.config.language || this.hass.locale.language
).substring(0, 2),
stt_engine: this._cloudActive ? "cloud" : undefined,
tts_engine: this._cloudActive ? "cloud" : undefined,
};
} }
let sstDefault: string | undefined;
let ttsDefault: string | undefined;
if (this._cloudActive) {
for (const entity of Object.values(this.hass.entities)) {
if (entity.platform !== "cloud") {
continue;
}
if (computeDomain(entity.entity_id) === "stt") {
sstDefault = entity.entity_id;
if (ttsDefault) {
break;
}
} else if (computeDomain(entity.entity_id) === "tts") {
ttsDefault = entity.entity_id;
if (sstDefault) {
break;
}
}
}
}
this._data = {
language: (
this.hass.config.language || this.hass.locale.language
).substring(0, 2),
stt_engine: sstDefault,
tts_engine: ttsDefault,
};
} }
public closeDialog(): void { public closeDialog(): void {

View File

@ -1,3 +1,4 @@
import { mdiAlertCircle } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@ -5,15 +6,16 @@ import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { mdiAlertCircle } from "@mdi/js";
import { computeCssColor } from "../../../common/color/compute-color"; import { computeCssColor } from "../../../common/color/compute-color";
import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color"; import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { stateActive } from "../../../common/entity/state_active"; import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color"; import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-ripple"; import "../../../components/ha-ripple";
import "../../../components/ha-state-icon"; import "../../../components/ha-state-icon";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { cameraUrlWithWidthHeight } from "../../../data/camera";
import { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; import { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive"; import { actionHandler } from "../common/directives/action-handler-directive";
@ -22,15 +24,38 @@ import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action"; import { hasAction } from "../common/has-action";
import { LovelaceBadge, LovelaceBadgeEditor } from "../types"; import { LovelaceBadge, LovelaceBadgeEditor } from "../types";
import { EntityBadgeConfig } from "./types"; import { EntityBadgeConfig } from "./types";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { cameraUrlWithWidthHeight } from "../../../data/camera";
export const DISPLAY_TYPES = ["minimal", "standard", "complete"] as const; export const DISPLAY_TYPES = ["minimal", "standard", "complete"] as const;
export type DisplayType = (typeof DISPLAY_TYPES)[number]; export type DisplayType = (typeof DISPLAY_TYPES)[number];
export const DEFAULT_DISPLAY_TYPE: DisplayType = "standard"; export const DEFAULT_DISPLAY_TYPE: DisplayType = "standard";
export const DEFAULT_CONFIG: EntityBadgeConfig = {
type: "entity",
show_name: false,
show_state: true,
show_icon: true,
};
export const migrateLegacyEntityBadgeConfig = (
config: EntityBadgeConfig
): EntityBadgeConfig => {
const newConfig = { ...config };
if (config.display_type) {
if (config.show_name === undefined) {
if (config.display_type === "complete") {
newConfig.show_name = true;
}
}
if (config.show_state === undefined) {
if (config.display_type === "minimal") {
newConfig.show_state = false;
}
}
delete newConfig.display_type;
}
return newConfig;
};
@customElement("hui-entity-badge") @customElement("hui-entity-badge")
export class HuiEntityBadge extends LitElement implements LovelaceBadge { export class HuiEntityBadge extends LitElement implements LovelaceBadge {
public static async getConfigElement(): Promise<LovelaceBadgeEditor> { public static async getConfigElement(): Promise<LovelaceBadgeEditor> {
@ -64,7 +89,10 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
@state() protected _config?: EntityBadgeConfig; @state() protected _config?: EntityBadgeConfig;
public setConfig(config: EntityBadgeConfig): void { public setConfig(config: EntityBadgeConfig): void {
this._config = config; this._config = {
...DEFAULT_CONFIG,
...migrateLegacyEntityBadgeConfig(config),
};
} }
get hasAction() { get hasAction() {
@ -134,9 +162,9 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
return html` return html`
<div class="badge error"> <div class="badge error">
<ha-svg-icon .hass=${this.hass} .path=${mdiAlertCircle}></ha-svg-icon> <ha-svg-icon .hass=${this.hass} .path=${mdiAlertCircle}></ha-svg-icon>
<span class="content"> <span class="info">
<span class="name">${entityId}</span> <span class="label">${entityId}</span>
<span class="state"> <span class="content">
${this.hass.localize("ui.badge.entity.not_found")} ${this.hass.localize("ui.badge.entity.not_found")}
</span> </span>
</span> </span>
@ -163,18 +191,25 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
const name = this._config.name || stateObj.attributes.friendly_name; const name = this._config.name || stateObj.attributes.friendly_name;
const displayType = this._config.display_type || DEFAULT_DISPLAY_TYPE; const showState = this._config.show_state;
const showName = this._config.show_name;
const showIcon = this._config.show_icon;
const showEntityPicture = this._config.show_entity_picture;
const imageUrl = this._config.show_entity_picture const imageUrl = showEntityPicture
? this._getImageUrl(stateObj) ? this._getImageUrl(stateObj)
: undefined; : undefined;
const label = showState && showName ? name : undefined;
const content = showState ? stateDisplay : showName ? name : undefined;
return html` return html`
<div <div
style=${styleMap(style)} style=${styleMap(style)}
class="badge ${classMap({ class="badge ${classMap({
active, active,
[displayType]: true, "no-info": !showState && !showName,
"no-icon": !showIcon,
})}" })}"
@action=${this._handleAction} @action=${this._handleAction}
.actionHandler=${actionHandler({ .actionHandler=${actionHandler({
@ -185,22 +220,22 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
tabindex=${ifDefined(this.hasAction ? "0" : undefined)} tabindex=${ifDefined(this.hasAction ? "0" : undefined)}
> >
<ha-ripple .disabled=${!this.hasAction}></ha-ripple> <ha-ripple .disabled=${!this.hasAction}></ha-ripple>
${imageUrl ${showIcon
? html`<img src=${imageUrl} aria-hidden />` ? imageUrl
: html` ? html`<img src=${imageUrl} aria-hidden />`
<ha-state-icon : html`
.hass=${this.hass} <ha-state-icon
.stateObj=${stateObj} .hass=${this.hass}
.icon=${this._config.icon} .stateObj=${stateObj}
></ha-state-icon> .icon=${this._config.icon}
`} ></ha-state-icon>
${displayType !== "minimal" `
: nothing}
${content
? html` ? html`
<span class="content"> <span class="info">
${displayType === "complete" ${label ? html`<span class="label">${name}</span>` : nothing}
? html`<span class="name">${name}</span>` <span class="content">${content}</span>
: nothing}
<span class="state">${stateDisplay}</span>
</span> </span>
` `
: nothing} : nothing}
@ -234,12 +269,15 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 8px; gap: 8px;
height: 36px; height: var(--ha-badge-size, 36px);
min-width: 36px; min-width: var(--ha-badge-size, 36px);
padding: 0px 8px; padding: 0px 8px;
box-sizing: border-box; box-sizing: border-box;
width: auto; width: auto;
border-radius: 18px; border-radius: var(
--ha-badge-border-radius,
calc(var(--ha-badge-size, 36px) / 2)
);
background: var( background: var(
--ha-card-background, --ha-card-background,
var(--card-background-color, white) var(--card-background-color, white)
@ -274,7 +312,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
.badge.active { .badge.active {
--badge-color: var(--primary-color); --badge-color: var(--primary-color);
} }
.content { .info {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
@ -282,7 +320,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
padding-inline-end: 4px; padding-inline-end: 4px;
padding-inline-start: initial; padding-inline-start: initial;
} }
.name { .label {
font-size: 10px; font-size: 10px;
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
@ -290,7 +328,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
letter-spacing: 0.1px; letter-spacing: 0.1px;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
.state { .content {
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
@ -310,14 +348,20 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
object-fit: cover; object-fit: cover;
overflow: hidden; overflow: hidden;
} }
.badge.minimal { .badge.no-info {
padding: 0; padding: 0;
} }
.badge:not(.minimal) img { .badge:not(.no-icon):not(.no-info) img {
margin-left: -6px; margin-left: -6px;
margin-inline-start: -6px; margin-inline-start: -6px;
margin-inline-end: initial; margin-inline-end: initial;
} }
.badge.no-icon .info {
padding-right: 4px;
padding-left: 4px;
padding-inline-end: 4px;
padding-inline-start: 4px;
}
`; `;
} }
} }

View File

@ -16,10 +16,10 @@ export class HuiStateLabelBadge extends HuiEntityBadge {
const entityBadgeConfig: EntityBadgeConfig = { const entityBadgeConfig: EntityBadgeConfig = {
type: "entity", type: "entity",
entity: config.entity, entity: config.entity,
display_type: config.show_name === false ? "standard" : "complete", show_name: config.show_name ?? true,
}; };
this._config = entityBadgeConfig; super.setConfig(entityBadgeConfig);
} }
} }

View File

@ -3,6 +3,7 @@ import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
import type { LegacyStateFilter } from "../common/evaluate-filter"; import type { LegacyStateFilter } from "../common/evaluate-filter";
import type { Condition } from "../common/validate-condition"; import type { Condition } from "../common/validate-condition";
import type { EntityFilterEntityConfig } from "../entity-rows/types"; import type { EntityFilterEntityConfig } from "../entity-rows/types";
import type { DisplayType } from "./hui-entity-badge";
export interface EntityFilterBadgeConfig extends LovelaceBadgeConfig { export interface EntityFilterBadgeConfig extends LovelaceBadgeConfig {
type: "entity-filter"; type: "entity-filter";
@ -33,10 +34,16 @@ export interface EntityBadgeConfig extends LovelaceBadgeConfig {
name?: string; name?: string;
icon?: string; icon?: string;
color?: string; color?: string;
show_name?: boolean;
show_state?: boolean;
show_icon?: boolean;
show_entity_picture?: boolean; show_entity_picture?: boolean;
display_type?: "minimal" | "standard" | "complete";
state_content?: string | string[]; state_content?: string | string[];
tap_action?: ActionConfig; tap_action?: ActionConfig;
hold_action?: ActionConfig; hold_action?: ActionConfig;
double_tap_action?: ActionConfig; double_tap_action?: ActionConfig;
/**
* @deprecated use `show_state`, `show_name`, `icon_type`
*/
display_type?: DisplayType;
} }

View File

@ -18,18 +18,22 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { getGraphColorByIndex } from "../../../../common/color/colors"; import { getGraphColorByIndex } from "../../../../common/color/colors";
import { getEnergyColor } from "./common/color";
import { ChartDatasetExtra } from "../../../../components/chart/ha-chart-base"; import { ChartDatasetExtra } from "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import { import {
DeviceConsumptionEnergyPreference, DeviceConsumptionEnergyPreference,
EnergyData, EnergyData,
getEnergyDataCollection, getEnergyDataCollection,
getSummedData,
computeConsumptionData,
} from "../../../../data/energy"; } from "../../../../data/energy";
import { import {
calculateStatisticSumGrowth, calculateStatisticSumGrowth,
getStatisticLabel, getStatisticLabel,
Statistics, Statistics,
StatisticsMetaData, StatisticsMetaData,
isExternalStatistic,
} from "../../../../data/recorder"; } from "../../../../data/recorder";
import { FrontendLocaleData } from "../../../../data/translation"; import { FrontendLocaleData } from "../../../../data/translation";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
@ -38,7 +42,9 @@ import { LovelaceCard } from "../../types";
import { EnergyDevicesDetailGraphCardConfig } from "../types"; import { EnergyDevicesDetailGraphCardConfig } from "../types";
import { hasConfigChanged } from "../../common/has-changed"; import { hasConfigChanged } from "../../common/has-changed";
import { getCommonOptions } from "./common/energy-chart-options"; import { getCommonOptions } from "./common/energy-chart-options";
import { fireEvent } from "../../../../common/dom/fire_event";
import { storage } from "../../../../common/decorators/storage"; import { storage } from "../../../../common/decorators/storage";
import { clickIsTouch } from "../../../../components/chart/click_is_touch";
const UNIT = "kWh"; const UNIT = "kWh";
@ -72,6 +78,8 @@ export class HuiEnergyDevicesDetailGraphCard
}) })
private _hiddenStats: string[] = []; private _hiddenStats: string[] = [];
private _untrackedIndex?: number;
protected hassSubscribeRequiredHostProps = ["_config"]; protected hassSubscribeRequiredHostProps = ["_config"];
public hassSubscribe(): UnsubscribeFunc[] { public hassSubscribe(): UnsubscribeFunc[] {
@ -149,17 +157,22 @@ export class HuiEnergyDevicesDetailGraphCard
} }
private _datasetHidden(ev) { private _datasetHidden(ev) {
this._hiddenStats = [ const hiddenEntity =
...this._hiddenStats, ev.detail.index === this._untrackedIndex
this._data!.prefs.device_consumption[ev.detail.index].stat_consumption, ? "untracked"
]; : this._data!.prefs.device_consumption[ev.detail.index]
.stat_consumption;
this._hiddenStats = [...this._hiddenStats, hiddenEntity];
} }
private _datasetUnhidden(ev) { private _datasetUnhidden(ev) {
const hiddenEntity =
ev.detail.index === this._untrackedIndex
? "untracked"
: this._data!.prefs.device_consumption[ev.detail.index]
.stat_consumption;
this._hiddenStats = this._hiddenStats.filter( this._hiddenStats = this._hiddenStats.filter(
(stat) => (stat) => stat !== hiddenEntity
stat !==
this._data!.prefs.device_consumption[ev.detail.index].stat_consumption
); );
} }
@ -197,6 +210,20 @@ export class HuiEnergyDevicesDetailGraphCard
}, },
}, },
}, },
onClick: (event, elements, chart) => {
if (clickIsTouch(event)) return;
const index = elements[0]?.datasetIndex ?? -1;
if (index < 0) return;
const statisticId =
this._data?.prefs.device_consumption[index]?.stat_consumption;
if (!statisticId || isExternalStatistic(statisticId)) return;
fireEvent(this, "hass-more-info", { entityId: statisticId });
chart?.canvas?.dispatchEvent(new Event("mouseout")); // to hide tooltip
},
}; };
return options; return options;
} }
@ -240,6 +267,33 @@ export class HuiEnergyDevicesDetailGraphCard
datasetExtras.push(...processedDataExtras); datasetExtras.push(...processedDataExtras);
const { summedData, compareSummedData } = getSummedData(energyData);
const showUntracked =
"from_grid" in summedData ||
"solar" in summedData ||
"from_battery" in summedData;
const {
consumption: consumptionData,
compareConsumption: consumptionCompareData,
} = showUntracked
? computeConsumptionData(summedData, compareSummedData)
: { consumption: undefined, compareConsumption: undefined };
if (showUntracked) {
this._untrackedIndex = datasets.length;
const { dataset: untrackedData, datasetExtra: untrackedDataExtra } =
this._processUntracked(
computedStyle,
processedData,
consumptionData,
false
);
datasets.push(untrackedData);
datasetExtras.push(untrackedDataExtra);
}
if (compareData) { if (compareData) {
// Add empty dataset to align the bars // Add empty dataset to align the bars
datasets.push({ datasets.push({
@ -272,6 +326,20 @@ export class HuiEnergyDevicesDetailGraphCard
datasets.push(...processedCompareData); datasets.push(...processedCompareData);
datasetExtras.push(...processedCompareDataExtras); datasetExtras.push(...processedCompareDataExtras);
if (showUntracked) {
const {
dataset: untrackedCompareData,
datasetExtra: untrackedCompareDataExtra,
} = this._processUntracked(
computedStyle,
processedCompareData,
consumptionCompareData,
true
);
datasets.push(untrackedCompareData);
datasetExtras.push(untrackedCompareDataExtra);
}
} }
this._start = energyData.start; this._start = energyData.start;
@ -286,6 +354,59 @@ export class HuiEnergyDevicesDetailGraphCard
this._chartDatasetExtra = datasetExtras; this._chartDatasetExtra = datasetExtras;
} }
private _processUntracked(
computedStyle: CSSStyleDeclaration,
processedData,
consumptionData,
compare: boolean
): { dataset; datasetExtra } {
const totalDeviceConsumption: { [start: number]: number } = {};
processedData.forEach((device) => {
device.data.forEach((datapoint) => {
totalDeviceConsumption[datapoint.x] =
(totalDeviceConsumption[datapoint.x] || 0) + datapoint.y;
});
});
const untrackedConsumption: { x: number; y: number }[] = [];
Object.keys(consumptionData.total).forEach((time) => {
untrackedConsumption.push({
x: Number(time),
y: consumptionData.total[time] - (totalDeviceConsumption[time] || 0),
});
});
const dataset = {
label: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_detail_graph.untracked_consumption"
),
hidden: this._hiddenStats.includes("untracked"),
borderColor: getEnergyColor(
computedStyle,
this.hass.themes.darkMode,
false,
compare,
"--state-unavailable-color"
),
backgroundColor: getEnergyColor(
computedStyle,
this.hass.themes.darkMode,
true,
compare,
"--state-unavailable-color"
),
data: untrackedConsumption,
order: 1 + this._untrackedIndex!,
stack: "devices",
pointStyle: compare ? false : "circle",
xAxisID: compare ? "xAxisCompare" : undefined,
};
const datasetExtra = {
show_legend: !compare,
};
return { dataset, datasetExtra };
}
private _processDataSet( private _processDataSet(
computedStyle: CSSStyleDeclaration, computedStyle: CSSStyleDeclaration,
statistics: Statistics, statistics: Statistics,

Some files were not shown because too many files have changed in this diff Show More