mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-19 07:37:08 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 190ef6bb87 | |||
| 91b6a4c4b6 | |||
| 643cc4ca7d | |||
| 9ef71e6cf4 | |||
| bface72af7 | |||
| 90028b2e22 | |||
| 914c48abd5 | |||
| 79c082acde | |||
| 4728eb7231 | |||
| d02b92bd32 | |||
| 98525d23e6 | |||
| ec98b21276 | |||
| defad3beca |
+3
-4
@@ -62,7 +62,6 @@
|
||||
"@lit-labs/virtualizer": "2.1.1",
|
||||
"@lit/context": "1.1.6",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/mwc-base": "0.27.0",
|
||||
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch",
|
||||
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
@@ -75,8 +74,8 @@
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.21",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
"@tsparticles/engine": "4.0.1",
|
||||
"@tsparticles/preset-links": "4.0.1",
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
@@ -166,7 +165,7 @@
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
"del": "8.0.1",
|
||||
"eslint": "10.3.0",
|
||||
"eslint": "10.4.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.11",
|
||||
"eslint-plugin-import-x": "4.16.2",
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import type {
|
||||
ReactiveController,
|
||||
ReactiveControllerHost,
|
||||
} from "@lit/reactive-element/reactive-controller";
|
||||
import type {
|
||||
Condition,
|
||||
ConditionContext,
|
||||
} from "../../panels/lovelace/common/validate-condition";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { setupConditionListeners } from "../condition/listeners";
|
||||
|
||||
/**
|
||||
* Reactive controller that manages the media-query and time-based listeners
|
||||
* needed to keep a set of lovelace visibility conditions evaluated live.
|
||||
*
|
||||
* The host is responsible for the actual evaluation (e.g. computing visible /
|
||||
* hidden / invalid state); the controller only triggers it via the supplied
|
||||
* `onUpdate` callback when something the conditions depend on changes. Call
|
||||
* `setup()` whenever the conditions change; the controller clears previous
|
||||
* listeners and re-subscribes. Listeners are automatically released when the
|
||||
* host disconnects.
|
||||
*/
|
||||
export class ConditionListenersController implements ReactiveController {
|
||||
private _unsubs: (() => void)[] = [];
|
||||
|
||||
constructor(host: ReactiveControllerHost) {
|
||||
host.addController(this);
|
||||
}
|
||||
|
||||
public hostDisconnected(): void {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public setup(
|
||||
conditions: Condition[],
|
||||
hass: HomeAssistant,
|
||||
onUpdate: () => void,
|
||||
getContext?: () => ConditionContext
|
||||
): void {
|
||||
this.clear();
|
||||
if (!conditions.length) {
|
||||
return;
|
||||
}
|
||||
setupConditionListeners(
|
||||
conditions,
|
||||
hass,
|
||||
(unsub) => this._unsubs.push(unsub),
|
||||
() => onUpdate(),
|
||||
getContext
|
||||
);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
for (const unsub of this._unsubs) {
|
||||
unsub();
|
||||
}
|
||||
this._unsubs = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../ha-tooltip";
|
||||
|
||||
export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
|
||||
|
||||
/**
|
||||
* @element ha-automation-row-live-test
|
||||
*
|
||||
* @summary
|
||||
* Small status indicator dot used in automation/condition rows to surface the
|
||||
* live evaluation result. Renders an optional tooltip with details on hover.
|
||||
*
|
||||
* @attr {"pass"|"fail"|"invalid"|"unknown"} state - The current live-test state. Defaults to `unknown`.
|
||||
* @attr {string} label - Accessible label announced by assistive technology.
|
||||
* @attr {string} message - Optional tooltip body shown on hover/focus.
|
||||
*/
|
||||
@customElement("ha-automation-row-live-test")
|
||||
export class HaAutomationRowLiveTest extends LitElement {
|
||||
@property({ reflect: true }) public state: LiveTestState = "unknown";
|
||||
|
||||
@property() public label = "";
|
||||
|
||||
@property() public message?: string;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div
|
||||
id="indicator"
|
||||
role="status"
|
||||
tabindex="0"
|
||||
aria-label=${this.label}
|
||||
></div>
|
||||
${this.message
|
||||
? html`<ha-tooltip for="indicator">${this.message}</ha-tooltip>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: absolute;
|
||||
inset-inline-end: -6px;
|
||||
display: inline-block;
|
||||
}
|
||||
#indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
border: 3px solid;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--card-background-color);
|
||||
transition: all var(--ha-animation-duration-normal) ease-in-out;
|
||||
}
|
||||
:host([state="pass"]) #indicator {
|
||||
background-color: var(--ha-color-fill-success-loud-resting);
|
||||
border-color: var(--ha-color-fill-success-loud-resting);
|
||||
}
|
||||
:host([state="pass"]) #indicator:hover {
|
||||
background-color: var(--ha-color-fill-success-loud-hover);
|
||||
border-color: var(--ha-color-fill-success-loud-hover);
|
||||
}
|
||||
:host([state="fail"]) #indicator {
|
||||
border-color: var(--ha-color-fill-warning-loud-resting);
|
||||
}
|
||||
:host([state="fail"]) #indicator:hover {
|
||||
background-color: var(--ha-color-fill-warning-loud-hover);
|
||||
border-color: var(--ha-color-fill-warning-loud-hover);
|
||||
}
|
||||
:host([state="invalid"]) #indicator {
|
||||
border-color: var(--ha-color-fill-danger-loud-resting);
|
||||
}
|
||||
:host([state="invalid"]) #indicator:hover {
|
||||
background-color: var(--ha-color-fill-danger-loud-hover);
|
||||
border-color: var(--ha-color-fill-danger-loud-hover);
|
||||
}
|
||||
:host([state="unknown"]) #indicator {
|
||||
border-color: var(--ha-color-fill-neutral-loud-resting);
|
||||
}
|
||||
:host([state="unknown"]) #indicator:hover {
|
||||
background-color: var(--ha-color-fill-neutral-loud-hover);
|
||||
border-color: var(--ha-color-fill-neutral-loud-hover);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-row-live-test": HaAutomationRowLiveTest;
|
||||
}
|
||||
}
|
||||
@@ -148,6 +148,7 @@ export interface GridSourceTypeEnergyPreference {
|
||||
power_config?: PowerConfig;
|
||||
|
||||
cost_adjustment_day: number;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface SolarSourceTypeEnergyPreference {
|
||||
@@ -156,6 +157,7 @@ export interface SolarSourceTypeEnergyPreference {
|
||||
stat_energy_from: string;
|
||||
stat_rate?: string;
|
||||
config_entry_solar_forecast: string[] | null;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface BatterySourceTypeEnergyPreference {
|
||||
@@ -165,6 +167,7 @@ export interface BatterySourceTypeEnergyPreference {
|
||||
stat_rate?: string; // always available if power_config is set
|
||||
power_config?: PowerConfig;
|
||||
stat_soc?: string;
|
||||
name?: string;
|
||||
}
|
||||
export interface GasSourceTypeEnergyPreference {
|
||||
type: "gas";
|
||||
|
||||
@@ -38,7 +38,7 @@ import { stateActive } from "../common/entity/state_active";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import type { MediaPlayerItemId } from "../components/media-player/ha-media-player-browse";
|
||||
import type { HomeAssistant, TranslationDict } from "../types";
|
||||
import { isUnavailableState } from "./entity/entity";
|
||||
import { UNAVAILABLE } from "./entity/entity";
|
||||
import { isTTSMediaSource } from "./tts";
|
||||
|
||||
interface MediaPlayerEntityAttributes extends HassEntityAttributeBase {
|
||||
@@ -284,7 +284,8 @@ export const computeMediaControls = (
|
||||
|
||||
const state = stateObj.state;
|
||||
|
||||
if (isUnavailableState(state)) {
|
||||
// We only filter out `unavailable`, not `unknown`
|
||||
if (state === UNAVAILABLE) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { showJoinMediaPlayersDialog } from "../../../components/media-player/show-join-media-players-dialog";
|
||||
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
|
||||
import { isUnavailableState } from "../../../data/entity/entity";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type {
|
||||
MediaPickedEvent,
|
||||
MediaPlayerEntity,
|
||||
@@ -200,12 +200,13 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
protected _renderSourceControl() {
|
||||
if (
|
||||
!this.stateObj ||
|
||||
!supportsFeature(this.stateObj, MediaPlayerEntityFeature.SELECT_SOURCE) ||
|
||||
!this.stateObj.attributes.source_list?.length
|
||||
!supportsFeature(this.stateObj, MediaPlayerEntityFeature.SELECT_SOURCE)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const sourceList = this.stateObj.attributes.source_list || [];
|
||||
|
||||
return html`<ha-tooltip for="source-button">
|
||||
${this.hass.localize(`ui.card.media_player.source`)}
|
||||
</ha-tooltip>
|
||||
@@ -217,7 +218,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
.path=${mdiLoginVariant}
|
||||
>
|
||||
</ha-icon-button>
|
||||
${this.stateObj.attributes.source_list!.map(
|
||||
${sourceList.map(
|
||||
(source) =>
|
||||
html`<ha-dropdown-item
|
||||
.value=${source}
|
||||
@@ -275,7 +276,8 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
protected _renderGrouping() {
|
||||
if (
|
||||
!this.stateObj ||
|
||||
isUnavailableState(this.stateObj.state) ||
|
||||
// Compare against `unavailable` so we allow `unknown`
|
||||
this.stateObj.state === UNAVAILABLE ||
|
||||
!supportsFeature(this.stateObj, MediaPlayerEntityFeature.GROUPING)
|
||||
) {
|
||||
return nothing;
|
||||
@@ -315,7 +317,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (isUnavailableState(this.stateObj.state)) {
|
||||
if (this.stateObj.state === UNAVAILABLE) {
|
||||
return this._renderEmptyCover(this.hass.formatEntityState(this.stateObj));
|
||||
}
|
||||
|
||||
@@ -461,7 +463,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
: nothing}
|
||||
${this._renderVolumeControl()}
|
||||
<div class="controls-row">
|
||||
${!isUnavailableState(stateObj.state) &&
|
||||
${stateObj.state !== UNAVAILABLE &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)
|
||||
? this._renderControlButton(
|
||||
"browse_media",
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
mdiAlertCircleCheck,
|
||||
mdiAppleKeyboardCommand,
|
||||
mdiArrowDown,
|
||||
mdiArrowRightThin,
|
||||
mdiArrowUp,
|
||||
mdiCheckboxBlankOutline,
|
||||
mdiCheckboxOutline,
|
||||
@@ -333,10 +332,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
${type !== "condition" &&
|
||||
(this.action as NonConditionAction).continue_on_error === true
|
||||
? html`<ha-svg-icon
|
||||
class="arrow-right"
|
||||
.path=${mdiArrowRightThin}
|
||||
></ha-svg-icon
|
||||
><ha-svg-icon
|
||||
id="svg-icon"
|
||||
.path=${mdiAlertCircleCheck}
|
||||
></ha-svg-icon>
|
||||
@@ -1163,9 +1158,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
rowStyles,
|
||||
overflowStyles,
|
||||
css`
|
||||
ha-svg-icon.arrow-right {
|
||||
--icon-primary-color: var(--ha-color-fill-neutral-loud-resting);
|
||||
}
|
||||
ha-svg-icon#svg-icon {
|
||||
--icon-primary-color: var(--ha-color-fill-neutral-loud-active);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import type {
|
||||
} from "home-assistant-js-websocket";
|
||||
import { dump } from "js-yaml";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -39,6 +39,7 @@ import { debounce } from "../../../../common/util/debounce";
|
||||
import "../../../../components/automation/ha-automation-row";
|
||||
import type { HaAutomationRow } from "../../../../components/automation/ha-automation-row";
|
||||
import "../../../../components/automation/ha-automation-row-event-chip";
|
||||
import "../../../../components/automation/ha-automation-row-live-test";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-condition-icon";
|
||||
import "../../../../components/ha-dropdown";
|
||||
@@ -46,7 +47,6 @@ import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
@@ -498,23 +498,15 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
@click=${this._toggleSidebar}
|
||||
@toggle-collapsed=${this._toggleCollapse}
|
||||
>${this._renderRow()}
|
||||
<div
|
||||
<ha-automation-row-live-test
|
||||
slot="icons"
|
||||
id="live-test"
|
||||
class=${this._liveTestResult.state}
|
||||
role="status"
|
||||
tabindex="0"
|
||||
aria-label=${this.hass.localize(
|
||||
.state=${this._liveTestResult.state}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.live_test_state.${this._liveTestResult.state}`
|
||||
)}
|
||||
>
|
||||
${this._liveTestResult.message
|
||||
? html`<ha-tooltip for="live-test">
|
||||
${this._liveTestResult.message}
|
||||
</ha-tooltip>`
|
||||
: nothing}
|
||||
</div></ha-automation-row
|
||||
>`
|
||||
.message=${this._liveTestResult.message}
|
||||
></ha-automation-row-live-test
|
||||
></ha-automation-row>`
|
||||
: html`
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
@@ -1064,52 +1056,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
rowStyles,
|
||||
overflowStyles,
|
||||
css`
|
||||
#live-test {
|
||||
position: absolute;
|
||||
inset-inline-end: -6px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
border: 3px solid;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--card-background-color);
|
||||
transition: all var(--ha-animation-duration-normal) ease-in-out;
|
||||
}
|
||||
#live-test.pass {
|
||||
background-color: var(--ha-color-fill-success-loud-resting);
|
||||
border-color: var(--ha-color-fill-success-loud-resting);
|
||||
}
|
||||
#live-test.pass:hover {
|
||||
background-color: var(--ha-color-fill-success-loud-hover);
|
||||
border-color: var(--ha-color-fill-success-loud-hover);
|
||||
}
|
||||
#live-test.fail {
|
||||
border-color: var(--ha-color-fill-warning-loud-resting);
|
||||
}
|
||||
#live-test.fail:hover {
|
||||
background-color: var(--ha-color-fill-warning-loud-hover);
|
||||
border-color: var(--ha-color-fill-warning-loud-hover);
|
||||
}
|
||||
#live-test.invalid {
|
||||
border-color: var(--ha-color-fill-danger-loud-resting);
|
||||
}
|
||||
#live-test.invalid:hover {
|
||||
background-color: var(--ha-color-fill-danger-loud-hover);
|
||||
border-color: var(--ha-color-fill-danger-loud-hover);
|
||||
}
|
||||
#live-test.unknown {
|
||||
border-color: var(--ha-color-fill-neutral-loud-resting);
|
||||
}
|
||||
#live-test.unknown:hover {
|
||||
background-color: var(--ha-color-fill-neutral-loud-hover);
|
||||
border-color: var(--ha-color-fill-neutral-loud-hover);
|
||||
}
|
||||
`,
|
||||
];
|
||||
return [rowStyles, overflowStyles];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mdiBatteryHigh, mdiDelete, mdiPencil, mdiPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-card";
|
||||
@@ -100,19 +100,24 @@ export class EnergyBatterySettings extends LitElement {
|
||||
></ha-svg-icon>`}
|
||||
<div class="content">
|
||||
<span class="label"
|
||||
>${getStatisticLabel(
|
||||
>${source.name ||
|
||||
getStatisticLabel(
|
||||
this.hass,
|
||||
source.stat_energy_from,
|
||||
this.statsMetadata?.[source.stat_energy_from]
|
||||
)}</span
|
||||
>
|
||||
<span class="label"
|
||||
>${getStatisticLabel(
|
||||
this.hass,
|
||||
source.stat_energy_to,
|
||||
this.statsMetadata?.[source.stat_energy_to]
|
||||
)}</span
|
||||
>
|
||||
${source.name
|
||||
? nothing
|
||||
: html`
|
||||
<span class="label"
|
||||
>${getStatisticLabel(
|
||||
this.hass,
|
||||
source.stat_energy_to,
|
||||
this.statsMetadata?.[source.stat_energy_to]
|
||||
)}</span
|
||||
>
|
||||
`}
|
||||
</div>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
@@ -153,6 +158,7 @@ export class EnergyBatterySettings extends LitElement {
|
||||
|
||||
private _addSource() {
|
||||
showEnergySettingsBatteryDialog(this, {
|
||||
statsMetadata: this.statsMetadata,
|
||||
battery_sources: this.preferences.energy_sources.filter(
|
||||
(src) => src.type === "battery"
|
||||
) as BatterySourceTypeEnergyPreference[],
|
||||
@@ -169,6 +175,7 @@ export class EnergyBatterySettings extends LitElement {
|
||||
const origSource: BatterySourceTypeEnergyPreference =
|
||||
ev.currentTarget.closest(".row").source;
|
||||
showEnergySettingsBatteryDialog(this, {
|
||||
statsMetadata: this.statsMetadata,
|
||||
source: { ...origSource },
|
||||
battery_sources: this.preferences.energy_sources.filter(
|
||||
(src) => src.type === "battery"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiDelete,
|
||||
mdiWater,
|
||||
mdiDragHorizontalVariant,
|
||||
@@ -6,7 +7,7 @@ import {
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
@@ -15,10 +16,12 @@ import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import type {
|
||||
DeviceConsumptionEnergyPreference,
|
||||
EnergyPreferences,
|
||||
EnergyPreferencesValidation,
|
||||
EnergyValidationIssue,
|
||||
} from "../../../../data/energy";
|
||||
import { saveEnergyPreferences } from "../../../../data/energy";
|
||||
import type { StatisticsMetaData } from "../../../../data/recorder";
|
||||
@@ -93,7 +96,7 @@ export class EnergyDeviceSettingsWater extends LitElement {
|
||||
${repeat(
|
||||
this.preferences.device_consumption_water,
|
||||
(device) => device.stat_consumption,
|
||||
(device) => html`
|
||||
(device, index) => html`
|
||||
<div class="row" .device=${device}>
|
||||
<div class="handle">
|
||||
<ha-svg-icon
|
||||
@@ -108,6 +111,12 @@ export class EnergyDeviceSettingsWater extends LitElement {
|
||||
this.statsMetadata?.[device.stat_consumption]
|
||||
)}</span
|
||||
>
|
||||
${this._renderIssueIndicator(
|
||||
this.validationResult?.device_consumption_water[
|
||||
index
|
||||
],
|
||||
index
|
||||
)}
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.edit")}
|
||||
@click=${this._editDevice}
|
||||
@@ -144,6 +153,31 @@ export class EnergyDeviceSettingsWater extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderIssueIndicator(
|
||||
issues: EnergyValidationIssue[] | undefined,
|
||||
index: number
|
||||
) {
|
||||
if (!issues?.length) {
|
||||
return nothing;
|
||||
}
|
||||
const titles = issues.map(
|
||||
(issue) =>
|
||||
this.hass.localize(`component.energy.issues.${issue.type}.title`) ||
|
||||
issue.type
|
||||
);
|
||||
const label = titles.join("\n");
|
||||
const id = `issue-icon-${index}`;
|
||||
return html`
|
||||
<ha-svg-icon
|
||||
id=${id}
|
||||
class="issue-icon"
|
||||
.path=${mdiAlertCircle}
|
||||
aria-label=${label}
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip .for=${id} placement="top">${label}</ha-tooltip>
|
||||
`;
|
||||
}
|
||||
|
||||
private _itemMoved(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
@@ -248,6 +282,9 @@ export class EnergyDeviceSettingsWater extends LitElement {
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
}
|
||||
.issue-icon {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiDelete,
|
||||
mdiDevices,
|
||||
mdiDragHorizontalVariant,
|
||||
@@ -6,7 +7,7 @@ import {
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
@@ -15,10 +16,12 @@ import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import type {
|
||||
DeviceConsumptionEnergyPreference,
|
||||
EnergyPreferences,
|
||||
EnergyPreferencesValidation,
|
||||
EnergyValidationIssue,
|
||||
} from "../../../../data/energy";
|
||||
import { saveEnergyPreferences } from "../../../../data/energy";
|
||||
import type { StatisticsMetaData } from "../../../../data/recorder";
|
||||
@@ -93,7 +96,7 @@ export class EnergyDeviceSettings extends LitElement {
|
||||
${repeat(
|
||||
this.preferences.device_consumption,
|
||||
(device) => device.stat_consumption,
|
||||
(device) => html`
|
||||
(device, index) => html`
|
||||
<div class="row" .device=${device}>
|
||||
<div class="handle">
|
||||
<ha-svg-icon
|
||||
@@ -108,6 +111,10 @@ export class EnergyDeviceSettings extends LitElement {
|
||||
this.statsMetadata?.[device.stat_consumption]
|
||||
)}</span
|
||||
>
|
||||
${this._renderIssueIndicator(
|
||||
this.validationResult?.device_consumption[index],
|
||||
index
|
||||
)}
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.edit")}
|
||||
@click=${this._editDevice}
|
||||
@@ -144,6 +151,31 @@ export class EnergyDeviceSettings extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderIssueIndicator(
|
||||
issues: EnergyValidationIssue[] | undefined,
|
||||
index: number
|
||||
) {
|
||||
if (!issues?.length) {
|
||||
return nothing;
|
||||
}
|
||||
const titles = issues.map(
|
||||
(issue) =>
|
||||
this.hass.localize(`component.energy.issues.${issue.type}.title`) ||
|
||||
issue.type
|
||||
);
|
||||
const label = titles.join("\n");
|
||||
const id = `issue-icon-${index}`;
|
||||
return html`
|
||||
<ha-svg-icon
|
||||
id=${id}
|
||||
class="issue-icon"
|
||||
.path=${mdiAlertCircle}
|
||||
aria-label=${label}
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip .for=${id} placement="top">${label}</ha-tooltip>
|
||||
`;
|
||||
}
|
||||
|
||||
private _itemMoved(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
@@ -244,6 +276,9 @@ export class EnergyDeviceSettings extends LitElement {
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
}
|
||||
.issue-icon {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -124,13 +124,16 @@ export class EnergyGridSettings extends LitElement {
|
||||
></ha-svg-icon>`}
|
||||
<div class="content">
|
||||
<span class="label"
|
||||
>${getStatisticLabel(
|
||||
>${source.name ||
|
||||
getStatisticLabel(
|
||||
this.hass,
|
||||
primaryStat,
|
||||
this.statsMetadata?.[primaryStat]
|
||||
)}</span
|
||||
>
|
||||
${source.stat_energy_from && source.stat_energy_to
|
||||
${source.stat_energy_from &&
|
||||
source.stat_energy_to &&
|
||||
!source.name
|
||||
? html`<span class="label secondary"
|
||||
>${getStatisticLabel(
|
||||
this.hass,
|
||||
@@ -266,6 +269,7 @@ export class EnergyGridSettings extends LitElement {
|
||||
|
||||
private _addSource() {
|
||||
showEnergySettingsGridDialog(this, {
|
||||
statsMetadata: this.statsMetadata,
|
||||
grid_sources: this._getGridSources(),
|
||||
saveCallback: async (source) => {
|
||||
const preferences: EnergyPreferences = {
|
||||
@@ -283,6 +287,7 @@ export class EnergyGridSettings extends LitElement {
|
||||
const sourceIndex: number = row.sourceIndex;
|
||||
|
||||
showEnergySettingsGridDialog(this, {
|
||||
statsMetadata: this.statsMetadata,
|
||||
source: { ...origSource },
|
||||
grid_sources: this._getGridSources(),
|
||||
saveCallback: async (newSource) => {
|
||||
|
||||
@@ -101,7 +101,8 @@ export class EnergySolarSettings extends LitElement {
|
||||
.path=${mdiSolarPower}
|
||||
></ha-svg-icon>`}
|
||||
<span class="content"
|
||||
>${getStatisticLabel(
|
||||
>${source.name ||
|
||||
getStatisticLabel(
|
||||
this.hass,
|
||||
source.stat_energy_from,
|
||||
this.statsMetadata?.[source.stat_energy_from]
|
||||
@@ -154,6 +155,7 @@ export class EnergySolarSettings extends LitElement {
|
||||
|
||||
private _addSource() {
|
||||
showEnergySettingsSolarDialog(this, {
|
||||
statsMetadata: this.statsMetadata,
|
||||
info: this.info!,
|
||||
solar_sources: this.preferences.energy_sources.filter(
|
||||
(src) => src.type === "solar"
|
||||
@@ -171,6 +173,7 @@ export class EnergySolarSettings extends LitElement {
|
||||
const origSource: SolarSourceTypeEnergyPreference =
|
||||
ev.currentTarget.closest(".row").source;
|
||||
showEnergySettingsSolarDialog(this, {
|
||||
statsMetadata: this.statsMetadata,
|
||||
info: this.info!,
|
||||
source: { ...origSource },
|
||||
solar_sources: this.preferences.energy_sources.filter(
|
||||
|
||||
@@ -6,6 +6,7 @@ import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/input/ha-input";
|
||||
import type {
|
||||
BatterySourceTypeEnergyPreference,
|
||||
PowerConfig,
|
||||
@@ -14,6 +15,11 @@ import {
|
||||
emptyBatteryEnergyPreference,
|
||||
energyStatisticHelpUrl,
|
||||
} from "../../../../data/energy";
|
||||
import {
|
||||
getStatisticLabel,
|
||||
getStatisticMetadata,
|
||||
isExternalStatistic,
|
||||
} from "../../../../data/recorder";
|
||||
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
@@ -27,6 +33,7 @@ import {
|
||||
type PowerType,
|
||||
} from "./ha-energy-power-config";
|
||||
import type { EnergySettingsBatteryDialogParams } from "./show-dialogs-energy";
|
||||
import type { HaInput } from "../../../../components/input/ha-input";
|
||||
|
||||
const energyUnitClasses = ["energy"];
|
||||
const socStatisticsUnits = ["%"];
|
||||
@@ -174,6 +181,32 @@ export class DialogEnergyBatterySettings
|
||||
)}
|
||||
></ha-statistic-picker>
|
||||
|
||||
<ha-input
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.display_name"
|
||||
)}
|
||||
type="text"
|
||||
.disabled=${!(
|
||||
this._source?.stat_energy_from || this._source?.stat_energy_to
|
||||
)}
|
||||
.value=${this._source?.name || ""}
|
||||
.placeholder=${this._source?.stat_energy_from
|
||||
? getStatisticLabel(
|
||||
this.hass,
|
||||
this._source.stat_energy_from,
|
||||
this._params?.statsMetadata?.[this._source.stat_energy_from]
|
||||
)
|
||||
: this._source?.stat_energy_to
|
||||
? getStatisticLabel(
|
||||
this.hass,
|
||||
this._source.stat_energy_to,
|
||||
this._params?.statsMetadata?.[this._source.stat_energy_to]
|
||||
)
|
||||
: ""}
|
||||
@input=${this._nameChanged}
|
||||
>
|
||||
</ha-input>
|
||||
|
||||
<ha-energy-power-config
|
||||
.hass=${this.hass}
|
||||
.powerType=${this._powerType}
|
||||
@@ -232,12 +265,39 @@ export class DialogEnergyBatterySettings
|
||||
return true;
|
||||
}
|
||||
|
||||
private async _updateMetadata(statId: string) {
|
||||
if (
|
||||
statId &&
|
||||
isExternalStatistic(statId) &&
|
||||
this._params?.statsMetadata &&
|
||||
!(statId in this._params.statsMetadata)
|
||||
) {
|
||||
const [metadata] = await getStatisticMetadata(this.hass, [statId]);
|
||||
if (metadata) {
|
||||
this._params.statsMetadata[statId] = metadata;
|
||||
this.requestUpdate("_params");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _statisticToChanged(ev: ValueChangedEvent<string>) {
|
||||
this._source = { ...this._source!, stat_energy_to: ev.detail.value };
|
||||
this._updateMetadata(ev.detail.value);
|
||||
}
|
||||
|
||||
private _statisticFromChanged(ev: ValueChangedEvent<string>) {
|
||||
this._source = { ...this._source!, stat_energy_from: ev.detail.value };
|
||||
this._updateMetadata(ev.detail.value);
|
||||
}
|
||||
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
this._source = {
|
||||
...this._source!,
|
||||
name: (ev.target as HaInput).value,
|
||||
};
|
||||
if (!this._source.name) {
|
||||
delete this._source.name;
|
||||
}
|
||||
}
|
||||
|
||||
private _handlePowerConfigChanged(
|
||||
@@ -261,6 +321,9 @@ export class DialogEnergyBatterySettings
|
||||
stat_energy_from: this._source!.stat_energy_from,
|
||||
stat_energy_to: this._source!.stat_energy_to,
|
||||
};
|
||||
if (this._source?.name) {
|
||||
source.name = this._source.name;
|
||||
}
|
||||
|
||||
// Only include power_config if a power type is selected
|
||||
if (this._powerType !== "none") {
|
||||
|
||||
@@ -19,7 +19,11 @@ import {
|
||||
emptyGridSourceEnergyPreference,
|
||||
energyStatisticHelpUrl,
|
||||
} from "../../../../data/energy";
|
||||
import { isExternalStatistic } from "../../../../data/recorder";
|
||||
import {
|
||||
getStatisticLabel,
|
||||
getStatisticMetadata,
|
||||
isExternalStatistic,
|
||||
} from "../../../../data/recorder";
|
||||
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
@@ -33,6 +37,7 @@ import {
|
||||
type PowerType,
|
||||
} from "./ha-energy-power-config";
|
||||
import type { EnergySettingsGridDialogParams } from "./show-dialogs-energy";
|
||||
import type { HaInput } from "../../../../components/input/ha-input";
|
||||
|
||||
const energyUnitClasses = ["energy"];
|
||||
|
||||
@@ -224,6 +229,33 @@ export class DialogEnergyGridSettings
|
||||
)}
|
||||
></ha-statistic-picker>
|
||||
|
||||
<ha-input
|
||||
class="name"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.display_name"
|
||||
)}
|
||||
type="text"
|
||||
.disabled=${!(
|
||||
this._source?.stat_energy_from || this._source?.stat_energy_to
|
||||
)}
|
||||
.value=${this._source?.name || ""}
|
||||
.placeholder=${this._source?.stat_energy_from
|
||||
? getStatisticLabel(
|
||||
this.hass,
|
||||
this._source.stat_energy_from,
|
||||
this._params?.statsMetadata?.[this._source.stat_energy_from]
|
||||
)
|
||||
: this._source?.stat_energy_to
|
||||
? getStatisticLabel(
|
||||
this.hass,
|
||||
this._source.stat_energy_to,
|
||||
this._params?.statsMetadata?.[this._source.stat_energy_to]
|
||||
)
|
||||
: ""}
|
||||
@input=${this._nameChanged}
|
||||
>
|
||||
</ha-input>
|
||||
|
||||
<p class="section-label">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.import_cost"
|
||||
@@ -444,6 +476,21 @@ export class DialogEnergyGridSettings
|
||||
return true;
|
||||
}
|
||||
|
||||
private async _updateMetadata(statId: string) {
|
||||
if (
|
||||
statId &&
|
||||
isExternalStatistic(statId) &&
|
||||
this._params?.statsMetadata &&
|
||||
!(statId in this._params.statsMetadata)
|
||||
) {
|
||||
const [metadata] = await getStatisticMetadata(this.hass, [statId]);
|
||||
if (metadata) {
|
||||
this._params.statsMetadata[statId] = metadata;
|
||||
this.requestUpdate("_params");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _statisticFromChanged(ev: ValueChangedEvent<string>) {
|
||||
this._source = { ...this._source!, stat_energy_from: ev.detail.value };
|
||||
// Reset cost type if switching to external statistic with incompatible cost type
|
||||
@@ -459,6 +506,7 @@ export class DialogEnergyGridSettings
|
||||
number_energy_price: null,
|
||||
};
|
||||
}
|
||||
this._updateMetadata(ev.detail.value);
|
||||
}
|
||||
|
||||
private _statisticToChanged(ev: ValueChangedEvent<string>) {
|
||||
@@ -487,6 +535,17 @@ export class DialogEnergyGridSettings
|
||||
number_energy_price_export: null,
|
||||
};
|
||||
}
|
||||
this._updateMetadata(ev.detail.value);
|
||||
}
|
||||
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
this._source = {
|
||||
...this._source!,
|
||||
name: (ev.target as HaInput).value,
|
||||
};
|
||||
if (!this._source.name) {
|
||||
delete this._source.name;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleImportCostTypeChanged(ev: Event) {
|
||||
@@ -569,6 +628,9 @@ export class DialogEnergyGridSettings
|
||||
number_energy_price_export: this._source!.number_energy_price_export,
|
||||
cost_adjustment_day: this._source!.cost_adjustment_day,
|
||||
};
|
||||
if (this._source?.name) {
|
||||
source.name = this._source.name;
|
||||
}
|
||||
|
||||
// Only include power_config if a power type is selected
|
||||
if (this._powerType !== "none") {
|
||||
@@ -601,6 +663,9 @@ export class DialogEnergyGridSettings
|
||||
ha-input:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
ha-input.name {
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
ha-radio-group {
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/radio/ha-radio-group";
|
||||
import "../../../../components/input/ha-input";
|
||||
import type { HaRadioGroup } from "../../../../components/radio/ha-radio-group";
|
||||
import "../../../../components/radio/ha-radio-option";
|
||||
import type { ConfigEntry } from "../../../../data/config_entries";
|
||||
@@ -27,6 +28,12 @@ import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
import { brandsUrl } from "../../../../util/brands-url";
|
||||
import type { EnergySettingsSolarDialogParams } from "./show-dialogs-energy";
|
||||
import {
|
||||
getStatisticLabel,
|
||||
getStatisticMetadata,
|
||||
isExternalStatistic,
|
||||
} from "../../../../data/recorder";
|
||||
import type { HaInput } from "../../../../components/input/ha-input";
|
||||
|
||||
const energyUnitClasses = ["energy"];
|
||||
const powerUnitClasses = ["power"];
|
||||
@@ -129,6 +136,24 @@ export class DialogEnergySolarSettings
|
||||
autofocus
|
||||
></ha-statistic-picker>
|
||||
|
||||
<ha-input
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.solar.dialog.display_name"
|
||||
)}
|
||||
type="text"
|
||||
.disabled=${!this._source?.stat_energy_from}
|
||||
.value=${this._source?.name || ""}
|
||||
.placeholder=${this._source?.stat_energy_from
|
||||
? getStatisticLabel(
|
||||
this.hass,
|
||||
this._source.stat_energy_from,
|
||||
this._params?.statsMetadata?.[this._source.stat_energy_from]
|
||||
)
|
||||
: ""}
|
||||
@input=${this._nameChanged}
|
||||
>
|
||||
</ha-input>
|
||||
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.includeUnitClass=${powerUnitClasses}
|
||||
@@ -284,14 +309,38 @@ export class DialogEnergySolarSettings
|
||||
});
|
||||
}
|
||||
|
||||
private _statisticChanged(ev: ValueChangedEvent<string>) {
|
||||
private async _statisticChanged(ev: ValueChangedEvent<string>) {
|
||||
this._source = { ...this._source!, stat_energy_from: ev.detail.value };
|
||||
if (
|
||||
ev.detail.value &&
|
||||
isExternalStatistic(ev.detail.value) &&
|
||||
this._params?.statsMetadata &&
|
||||
!(ev.detail.value in this._params.statsMetadata)
|
||||
) {
|
||||
const [metadata] = await getStatisticMetadata(this.hass, [
|
||||
ev.detail.value,
|
||||
]);
|
||||
if (metadata) {
|
||||
this._params.statsMetadata[ev.detail.value] = metadata;
|
||||
this.requestUpdate("_params");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _powerStatisticChanged(ev: ValueChangedEvent<string>) {
|
||||
this._source = { ...this._source!, stat_rate: ev.detail.value };
|
||||
}
|
||||
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
this._source = {
|
||||
...this._source!,
|
||||
name: (ev.target as HaInput).value,
|
||||
};
|
||||
if (!this._source.name) {
|
||||
delete this._source.name;
|
||||
}
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
try {
|
||||
if (!this._forecast) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { StatisticsMetaData } from "../../../../data/recorder";
|
||||
export interface EnergySettingsGridDialogParams {
|
||||
source?: GridSourceTypeEnergyPreference;
|
||||
grid_sources: GridSourceTypeEnergyPreference[];
|
||||
statsMetadata?: Record<string, StatisticsMetaData>;
|
||||
saveCallback: (source: GridSourceTypeEnergyPreference) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -21,12 +22,14 @@ export interface EnergySettingsSolarDialogParams {
|
||||
info: EnergyInfo;
|
||||
source?: SolarSourceTypeEnergyPreference;
|
||||
solar_sources: SolarSourceTypeEnergyPreference[];
|
||||
statsMetadata?: Record<string, StatisticsMetaData>;
|
||||
saveCallback: (source: SolarSourceTypeEnergyPreference) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface EnergySettingsBatteryDialogParams {
|
||||
source?: BatterySourceTypeEnergyPreference;
|
||||
battery_sources: BatterySourceTypeEnergyPreference[];
|
||||
statsMetadata?: Record<string, StatisticsMetaData>;
|
||||
saveCallback: (source: BatterySourceTypeEnergyPreference) => Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,7 @@ export const supportsMediaPlayerSourceCardFeature = (
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return (
|
||||
domain === "media_player" &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.SELECT_SOURCE) &&
|
||||
!!stateObj.attributes.source_list?.length
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.SELECT_SOURCE)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -355,11 +355,13 @@ export class HuiEnergySolarGraphCard
|
||||
name: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_solar_graph.production",
|
||||
{
|
||||
name: getStatisticLabel(
|
||||
this.hass,
|
||||
source.stat_energy_from,
|
||||
statisticsMetaData[source.stat_energy_from]
|
||||
),
|
||||
name:
|
||||
source.name ||
|
||||
getStatisticLabel(
|
||||
this.hass,
|
||||
source.stat_energy_from,
|
||||
statisticsMetaData[source.stat_energy_from]
|
||||
),
|
||||
}
|
||||
),
|
||||
barMaxWidth: 50,
|
||||
@@ -463,11 +465,13 @@ export class HuiEnergySolarGraphCard
|
||||
name: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_solar_graph.forecast",
|
||||
{
|
||||
name: getStatisticLabel(
|
||||
this.hass,
|
||||
source.stat_energy_from,
|
||||
statisticsMetaData[source.stat_energy_from]
|
||||
),
|
||||
name:
|
||||
source.name ||
|
||||
getStatisticLabel(
|
||||
this.hass,
|
||||
source.stat_energy_from,
|
||||
statisticsMetaData[source.stat_energy_from]
|
||||
),
|
||||
}
|
||||
),
|
||||
step: false,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// @ts-ignore
|
||||
import dataTableStyles from "@material/data-table/dist/mdc.data-table.min.css";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, unsafeCSS, nothing } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
@@ -551,7 +549,13 @@ export class HuiEnergySourcesTableCard
|
||||
null,
|
||||
null,
|
||||
showCosts,
|
||||
compare
|
||||
compare,
|
||||
source.name
|
||||
? this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_sources_table.named_battery_discharged",
|
||||
{ name: source.name }
|
||||
)
|
||||
: ""
|
||||
)}${this._renderRow(
|
||||
computedStyles,
|
||||
"battery_in",
|
||||
@@ -563,7 +567,13 @@ export class HuiEnergySourcesTableCard
|
||||
null,
|
||||
null,
|
||||
showCosts,
|
||||
compare
|
||||
compare,
|
||||
source.name
|
||||
? this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_sources_table.named_battery_charged",
|
||||
{ name: source.name }
|
||||
)
|
||||
: ""
|
||||
)}`;
|
||||
})}
|
||||
${types.battery
|
||||
@@ -630,6 +640,15 @@ export class HuiEnergySourcesTableCard
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const name = !source.name
|
||||
? ""
|
||||
: source.stat_energy_to
|
||||
? this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_sources_table.named_grid_imported",
|
||||
{ name: source.name }
|
||||
)
|
||||
: source.name;
|
||||
|
||||
return this._renderRow(
|
||||
computedStyles,
|
||||
"grid_consumption",
|
||||
@@ -641,7 +660,8 @@ export class HuiEnergySourcesTableCard
|
||||
cost,
|
||||
costCompare,
|
||||
showCosts,
|
||||
compare
|
||||
compare,
|
||||
name
|
||||
);
|
||||
})();
|
||||
|
||||
@@ -670,6 +690,15 @@ export class HuiEnergySourcesTableCard
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const name = !source.name
|
||||
? ""
|
||||
: source.stat_energy_from
|
||||
? this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_sources_table.named_grid_exported",
|
||||
{ name: source.name }
|
||||
)
|
||||
: source.name;
|
||||
|
||||
return this._renderRow(
|
||||
computedStyles,
|
||||
"grid_return",
|
||||
@@ -681,7 +710,8 @@ export class HuiEnergySourcesTableCard
|
||||
-cost,
|
||||
-costCompare,
|
||||
showCosts,
|
||||
compare
|
||||
compare,
|
||||
name
|
||||
);
|
||||
})();
|
||||
|
||||
@@ -756,63 +786,120 @@ export class HuiEnergySourcesTableCard
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
${unsafeCSS(dataTableStyles)}
|
||||
.mdc-data-table {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
}
|
||||
.mdc-data-table__header-cell,
|
||||
.mdc-data-table__cell {
|
||||
color: var(--primary-text-color);
|
||||
border-bottom-color: var(--divider-color);
|
||||
text-align: var(--float-start);
|
||||
}
|
||||
.mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
|
||||
background-color: rgba(var(--rgb-primary-text-color), 0.04);
|
||||
}
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.total {
|
||||
--mdc-typography-body2-font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
.total .mdc-data-table__cell {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
ha-card {
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
.card-header {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
.has-header {
|
||||
padding-top: 0;
|
||||
}
|
||||
.cell-bullet {
|
||||
width: 32px;
|
||||
padding-right: 0;
|
||||
padding-inline-end: 0;
|
||||
padding-inline-start: 16px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.bullet {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
height: 16px;
|
||||
width: 32px;
|
||||
}
|
||||
.mdc-data-table__cell--numeric {
|
||||
direction: ltr;
|
||||
}
|
||||
`;
|
||||
}
|
||||
static styles: CSSResultGroup = css`
|
||||
.mdc-data-table__content,
|
||||
.mdc-data-table__cell {
|
||||
font-family: var(--ha-font-family-body);
|
||||
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
|
||||
-webkit-font-smoothing: var(--ha-font-smoothing);
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
letter-spacing: 0.0178571429em;
|
||||
text-decoration: inherit;
|
||||
text-transform: inherit;
|
||||
}
|
||||
.mdc-data-table {
|
||||
background-color: var(--card-background-color);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
border: 0;
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.mdc-data-table__table-container {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
}
|
||||
.mdc-data-table__table {
|
||||
min-width: 100%;
|
||||
border: 0;
|
||||
border-spacing: 0;
|
||||
table-layout: fixed;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mdc-data-table__header-row {
|
||||
height: 56px;
|
||||
}
|
||||
.mdc-data-table__row {
|
||||
background-color: inherit;
|
||||
height: 52px;
|
||||
}
|
||||
.mdc-data-table__header-cell,
|
||||
.mdc-data-table__cell {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
box-sizing: border-box;
|
||||
color: var(--primary-text-color);
|
||||
border-bottom-color: var(--divider-color);
|
||||
overflow: hidden;
|
||||
padding: 0 16px;
|
||||
text-align: var(--float-start);
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.mdc-data-table__header-cell {
|
||||
background-color: var(--card-background-color);
|
||||
font-family: var(--ha-font-family-body);
|
||||
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
|
||||
-webkit-font-smoothing: var(--ha-font-smoothing);
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
letter-spacing: 0.0071428571em;
|
||||
text-decoration: inherit;
|
||||
text-transform: inherit;
|
||||
}
|
||||
.mdc-data-table__row:last-child .mdc-data-table__cell {
|
||||
border-bottom: none;
|
||||
}
|
||||
.mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
|
||||
background-color: rgba(var(--rgb-primary-text-color), 0.04);
|
||||
}
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.total .mdc-data-table__cell {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
ha-card {
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
.card-header {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
.has-header {
|
||||
padding-top: 0;
|
||||
}
|
||||
.cell-bullet {
|
||||
width: 32px;
|
||||
padding-right: 0;
|
||||
padding-inline-end: 0;
|
||||
padding-inline-start: 16px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.bullet {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
height: 16px;
|
||||
width: 32px;
|
||||
}
|
||||
.mdc-data-table__cell--numeric {
|
||||
text-align: var(--float-end);
|
||||
direction: ltr;
|
||||
}
|
||||
.mdc-data-table__header-cell--numeric {
|
||||
text-align: var(--float-end);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -259,6 +259,16 @@ export class HuiEnergyUsageGraphCard
|
||||
from_battery?: string[];
|
||||
} = {};
|
||||
|
||||
const statLabels: {
|
||||
to_grid: Record<string, string>;
|
||||
from_grid: Record<string, string>;
|
||||
to_battery: Record<string, string>;
|
||||
} = {
|
||||
to_grid: {},
|
||||
from_grid: {},
|
||||
to_battery: {},
|
||||
};
|
||||
|
||||
for (const source of energyData.prefs.energy_sources) {
|
||||
if (source.type === "solar") {
|
||||
if (statIds.solar) {
|
||||
@@ -277,6 +287,12 @@ export class HuiEnergyUsageGraphCard
|
||||
statIds.to_battery = [source.stat_energy_to];
|
||||
statIds.from_battery = [source.stat_energy_from];
|
||||
}
|
||||
if (source.name) {
|
||||
statLabels.to_battery[source.stat_energy_to] = this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_sources_table.named_battery_charged",
|
||||
{ name: source.name }
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -291,6 +307,15 @@ export class HuiEnergyUsageGraphCard
|
||||
} else {
|
||||
statIds.from_grid = [gridSource.stat_energy_from];
|
||||
}
|
||||
if (gridSource.name) {
|
||||
statLabels.from_grid[gridSource.stat_energy_from] =
|
||||
gridSource.stat_energy_to
|
||||
? this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_usage_graph.named_grid_consumed",
|
||||
{ name: gridSource.name }
|
||||
)
|
||||
: gridSource.name;
|
||||
}
|
||||
}
|
||||
if (gridSource.stat_energy_to) {
|
||||
if (statIds.to_grid) {
|
||||
@@ -298,6 +323,15 @@ export class HuiEnergyUsageGraphCard
|
||||
} else {
|
||||
statIds.to_grid = [gridSource.stat_energy_to];
|
||||
}
|
||||
if (gridSource.name) {
|
||||
statLabels.to_grid[gridSource.stat_energy_to] =
|
||||
gridSource.stat_energy_from
|
||||
? this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_usage_graph.named_grid_exported",
|
||||
{ name: gridSource.name }
|
||||
)
|
||||
: gridSource.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,7 +354,7 @@ export class HuiEnergyUsageGraphCard
|
||||
}
|
||||
});
|
||||
|
||||
const labels = {
|
||||
const typeLabels = {
|
||||
used_grid: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_usage_graph.combined_from_grid"
|
||||
),
|
||||
@@ -354,7 +388,8 @@ export class HuiEnergyUsageGraphCard
|
||||
statIds,
|
||||
colorIndices,
|
||||
computedStyles,
|
||||
labels,
|
||||
typeLabels,
|
||||
statLabels,
|
||||
trackY,
|
||||
true
|
||||
)
|
||||
@@ -381,7 +416,8 @@ export class HuiEnergyUsageGraphCard
|
||||
statIds,
|
||||
colorIndices,
|
||||
computedStyles,
|
||||
labels,
|
||||
typeLabels,
|
||||
statLabels,
|
||||
trackY,
|
||||
false
|
||||
)
|
||||
@@ -415,11 +451,16 @@ export class HuiEnergyUsageGraphCard
|
||||
},
|
||||
colorIndices: Record<string, Record<string, number>>,
|
||||
computedStyles: CSSStyleDeclaration,
|
||||
labels: {
|
||||
typeLabels: {
|
||||
used_grid: string;
|
||||
used_solar: string;
|
||||
used_battery: string;
|
||||
},
|
||||
statLabels: {
|
||||
to_grid: Record<string, string>;
|
||||
from_grid: Record<string, string>;
|
||||
to_battery: Record<string, string>;
|
||||
},
|
||||
trackY: (v: number) => void,
|
||||
compare = false
|
||||
) {
|
||||
@@ -540,9 +581,10 @@ export class HuiEnergyUsageGraphCard
|
||||
type: "bar",
|
||||
cursor: "default",
|
||||
name:
|
||||
type in labels
|
||||
? labels[type]
|
||||
: getStatisticLabel(
|
||||
type in typeLabels
|
||||
? typeLabels[type]
|
||||
: statLabels[type]?.[statId] ||
|
||||
getStatisticLabel(
|
||||
this.hass,
|
||||
statId,
|
||||
statisticsMetaData[statId]
|
||||
|
||||
@@ -3,12 +3,12 @@ import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { Condition } from "../../common/validate-condition";
|
||||
import { conditionsEntityContext } from "../conditions/context";
|
||||
import "../conditions/ha-card-conditions-editor";
|
||||
import "../conditions/ha-visibility-status";
|
||||
|
||||
@customElement("hui-badge-visibility-editor")
|
||||
export class HuiBadgeVisibilityEditor extends LitElement {
|
||||
@@ -34,11 +34,10 @@ export class HuiBadgeVisibilityEditor extends LitElement {
|
||||
render() {
|
||||
const conditions = this.config.visibility ?? [];
|
||||
return html`
|
||||
<p class="intro">
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_badge.visibility.explanation`
|
||||
)}
|
||||
</p>
|
||||
<ha-visibility-status
|
||||
.hass=${this.hass}
|
||||
.conditions=${conditions}
|
||||
></ha-visibility-status>
|
||||
<ha-card-conditions-editor
|
||||
.hass=${this.hass}
|
||||
.conditions=${conditions}
|
||||
@@ -62,10 +61,8 @@ export class HuiBadgeVisibilityEditor extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.intro {
|
||||
margin: 0;
|
||||
color: var(--secondary-text-color);
|
||||
margin-bottom: 8px;
|
||||
ha-visibility-status {
|
||||
margin-bottom: var(--ha-space-3);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { Condition } from "../../common/validate-condition";
|
||||
import { conditionsEntityContext } from "../conditions/context";
|
||||
import "../conditions/ha-card-conditions-editor";
|
||||
import "../conditions/ha-visibility-status";
|
||||
|
||||
@customElement("hui-card-visibility-editor")
|
||||
export class HuiCardVisibilityEditor extends LitElement {
|
||||
@@ -34,11 +34,10 @@ export class HuiCardVisibilityEditor extends LitElement {
|
||||
render() {
|
||||
const conditions = this.config.visibility ?? [];
|
||||
return html`
|
||||
<p class="intro">
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_card.visibility.explanation`
|
||||
)}
|
||||
</p>
|
||||
<ha-visibility-status
|
||||
.hass=${this.hass}
|
||||
.conditions=${conditions}
|
||||
></ha-visibility-status>
|
||||
<ha-card-conditions-editor
|
||||
.hass=${this.hass}
|
||||
.conditions=${conditions}
|
||||
@@ -62,10 +61,8 @@ export class HuiCardVisibilityEditor extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.intro {
|
||||
margin: 0;
|
||||
color: var(--secondary-text-color);
|
||||
margin-bottom: 8px;
|
||||
ha-visibility-status {
|
||||
margin-bottom: var(--ha-space-3);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -13,12 +13,14 @@ import deepClone from "deep-clone-simple";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ConditionListenersController } from "../../../../common/controllers/condition-listeners-controller";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import "../../../../components/automation/ha-automation-row-event-chip";
|
||||
import "../../../../components/automation/ha-automation-row-live-test";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-dropdown";
|
||||
@@ -33,11 +35,11 @@ import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { ICON_CONDITION } from "../../common/icon-condition";
|
||||
import type {
|
||||
AndCondition,
|
||||
Condition,
|
||||
LegacyCondition,
|
||||
OrCondition,
|
||||
AndCondition,
|
||||
NotCondition,
|
||||
OrCondition,
|
||||
} from "../../common/validate-condition";
|
||||
import {
|
||||
checkConditionsMet,
|
||||
@@ -103,6 +105,13 @@ export class HaCardConditionEditor extends LitElement {
|
||||
|
||||
@state() private _testingResult?: boolean;
|
||||
|
||||
@state() private _liveTestResult: {
|
||||
state: "pass" | "fail" | "invalid" | "unknown";
|
||||
message?: string;
|
||||
} = { state: "unknown" };
|
||||
|
||||
private _listeners = new ConditionListenersController(this);
|
||||
|
||||
private get _editor() {
|
||||
if (!this._condition) return undefined;
|
||||
return customElements.get(
|
||||
@@ -116,6 +125,14 @@ export class HaCardConditionEditor extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _setupConditionListeners() {
|
||||
this._listeners.setup(
|
||||
this.condition ? [this.condition as Condition] : [],
|
||||
this.hass,
|
||||
() => this._evaluateLiveTest()
|
||||
);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
if (changedProperties.has("condition")) {
|
||||
this._condition = {
|
||||
@@ -143,7 +160,61 @@ export class HaCardConditionEditor extends LitElement {
|
||||
if (!this._uiAvailable && !this._yamlMode) {
|
||||
this._yamlMode = true;
|
||||
}
|
||||
|
||||
this._setupConditionListeners();
|
||||
}
|
||||
|
||||
if (changedProperties.has("condition") || changedProperties.has("hass")) {
|
||||
this._evaluateLiveTest();
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>): void {
|
||||
if ((changedProperties as Map<string, unknown>).has("_entityContext")) {
|
||||
this._evaluateLiveTest();
|
||||
}
|
||||
}
|
||||
|
||||
private _evaluateLiveTest() {
|
||||
if (!this.condition || !this._condition) {
|
||||
this._liveTestResult = { state: "unknown" };
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
isNoEntityCondition(this._condition.condition, this._noEntity) ||
|
||||
containsNoEntityCondition(this._condition, this._noEntity)
|
||||
) {
|
||||
this._liveTestResult = {
|
||||
state: "unknown",
|
||||
message: this.hass.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.live_test_state.unknown"
|
||||
),
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateConditionalConfig([this.condition])) {
|
||||
this._liveTestResult = {
|
||||
state: "invalid",
|
||||
message: this.hass.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.live_test_state.invalid"
|
||||
),
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const testContext =
|
||||
this._entityContext?.mode === "current"
|
||||
? { entity_id: this._entityContext.entityId }
|
||||
: {};
|
||||
const pass = checkConditionsMet([this.condition], this.hass, testContext);
|
||||
this._liveTestResult = {
|
||||
state: pass ? "pass" : "fail",
|
||||
message: this.hass.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.live_test_state.${pass ? "pass" : "fail"}`
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -151,6 +222,10 @@ export class HaCardConditionEditor extends LitElement {
|
||||
|
||||
if (!condition) return nothing;
|
||||
|
||||
const hideLiveTest =
|
||||
isNoEntityCondition(condition.condition, this._noEntity) ||
|
||||
containsNoEntityCondition(condition, this._noEntity);
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
<ha-expansion-panel left-chevron>
|
||||
@@ -164,6 +239,33 @@ export class HaCardConditionEditor extends LitElement {
|
||||
`ui.panel.lovelace.editor.condition-editor.condition.${condition.condition}.label`
|
||||
) || condition.condition}
|
||||
</h3>
|
||||
<ha-automation-row-event-chip
|
||||
.show=${this._testingResult !== undefined}
|
||||
.variant=${this._testingResult ? "success" : "warning"}
|
||||
slot="event"
|
||||
class="event-chip"
|
||||
aria-live="polite"
|
||||
>
|
||||
${this._testingResult
|
||||
? this.hass.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.testing_pass"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.testing_error"
|
||||
)}
|
||||
</ha-automation-row-event-chip>
|
||||
${hideLiveTest
|
||||
? nothing
|
||||
: html`
|
||||
<ha-automation-row-live-test
|
||||
slot="icons"
|
||||
.state=${this._liveTestResult.state}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.live_test_state.${this._liveTestResult.state}`
|
||||
)}
|
||||
.message=${this._liveTestResult.message}
|
||||
></ha-automation-row-live-test>
|
||||
`}
|
||||
<ha-dropdown
|
||||
slot="icons"
|
||||
@wa-select=${this._handleAction}
|
||||
@@ -267,23 +369,6 @@ export class HaCardConditionEditor extends LitElement {
|
||||
`}
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
<div
|
||||
class="testing ${classMap({
|
||||
active: this._testingResult !== undefined,
|
||||
pass: this._testingResult === true,
|
||||
error: this._testingResult === false,
|
||||
})}"
|
||||
>
|
||||
${this._testingResult
|
||||
? this.hass.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.testing_pass"
|
||||
)
|
||||
: this._testingResult === false
|
||||
? this.hass.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.testing_error"
|
||||
)
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -418,41 +503,9 @@ export class HaCardConditionEditor extends LitElement {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.testing {
|
||||
.event-chip {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
left: 0px;
|
||||
text-transform: uppercase;
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
background-color: var(--divider-color, #e0e0e0);
|
||||
color: var(--text-primary-color);
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s;
|
||||
text-align: center;
|
||||
border-top-right-radius: calc(
|
||||
var(--ha-card-border-radius, var(--ha-border-radius-lg)) - var(
|
||||
--ha-card-border-width,
|
||||
1px
|
||||
)
|
||||
);
|
||||
border-top-left-radius: calc(
|
||||
var(--ha-card-border-radius, var(--ha-border-radius-lg)) - var(
|
||||
--ha-card-border-width,
|
||||
1px
|
||||
)
|
||||
);
|
||||
}
|
||||
.testing.active {
|
||||
max-height: 100px;
|
||||
}
|
||||
.testing.error {
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
.testing.pass {
|
||||
background-color: var(--success-color);
|
||||
inset-inline-end: 40px;
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiAlertCircle, mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ConditionListenersController } from "../../../../common/controllers/condition-listeners-controller";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { HaRowItem } from "../../../../components/item/ha-row-item";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type {
|
||||
Condition,
|
||||
LegacyCondition,
|
||||
} from "../../common/validate-condition";
|
||||
import {
|
||||
checkConditionsMet,
|
||||
validateConditionalConfig,
|
||||
} from "../../common/validate-condition";
|
||||
import type { ConditionsEntityContext } from "./context";
|
||||
import { conditionsEntityContext } from "./context";
|
||||
|
||||
type VisibilityState = "visible" | "hidden" | "invalid";
|
||||
|
||||
const STATE_ICONS: Record<VisibilityState, string> = {
|
||||
visible: mdiEye,
|
||||
hidden: mdiEyeOff,
|
||||
invalid: mdiAlertCircle,
|
||||
};
|
||||
|
||||
/**
|
||||
* @element ha-visibility-status
|
||||
* @extends {HaRowItem}
|
||||
*
|
||||
* @summary
|
||||
* Row-style banner that surfaces the live visibility result for a set of
|
||||
* lovelace conditions. Replaces the static explanation alert at the top of
|
||||
* card / section / badge / conditional-card visibility editors.
|
||||
*
|
||||
* @attr {"visible"|"hidden"|"invalid"} state - Computed visibility state (reflected for styling).
|
||||
*/
|
||||
@customElement("ha-visibility-status")
|
||||
export class HaVisibilityStatus extends HaRowItem {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public conditions: (Condition | LegacyCondition)[] = [];
|
||||
|
||||
@state()
|
||||
@consume({ context: conditionsEntityContext, subscribe: true })
|
||||
private _entityContext?: ConditionsEntityContext;
|
||||
|
||||
@property({ reflect: true })
|
||||
public state: VisibilityState = "visible";
|
||||
|
||||
private _listeners = new ConditionListenersController(this);
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
super.willUpdate(changedProperties);
|
||||
if (changedProperties.has("conditions") || changedProperties.has("hass")) {
|
||||
this._listeners.setup(
|
||||
(this.conditions ?? []) as Condition[],
|
||||
this.hass,
|
||||
() => this._evaluate()
|
||||
);
|
||||
}
|
||||
if (
|
||||
changedProperties.has("hass") ||
|
||||
changedProperties.has("conditions") ||
|
||||
(changedProperties as Map<string, unknown>).has("_entityContext")
|
||||
) {
|
||||
this._evaluate();
|
||||
}
|
||||
}
|
||||
|
||||
protected override _renderInner(): TemplateResult {
|
||||
return html`
|
||||
<div part="start" class="start">
|
||||
<ha-svg-icon .path=${STATE_ICONS[this.state]}></ha-svg-icon>
|
||||
</div>
|
||||
<div part="content" class="content">
|
||||
<div part="headline" class="headline">
|
||||
${this.hass?.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.visibility_status.${this.state}.headline`
|
||||
)}
|
||||
</div>
|
||||
<div part="supporting-text" class="supporting">
|
||||
${this.hass?.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.visibility_status.${this.state}.supporting${(this.conditions?.length ?? 0) === 0 ? "_empty" : ""}`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _evaluate() {
|
||||
const conditions = this.conditions ?? [];
|
||||
let newState: VisibilityState;
|
||||
if (conditions.length === 0) {
|
||||
newState = "visible";
|
||||
} else if (!validateConditionalConfig(conditions)) {
|
||||
newState = "invalid";
|
||||
} else {
|
||||
const context =
|
||||
this._entityContext?.mode === "current"
|
||||
? { entity_id: this._entityContext.entityId }
|
||||
: {};
|
||||
newState = checkConditionsMet(conditions, this.hass, context)
|
||||
? "visible"
|
||||
: "hidden";
|
||||
}
|
||||
if (newState === this.state) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state = newState;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = [
|
||||
HaRowItem.styles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
border-radius: var(--ha-border-radius-xl);
|
||||
transition: background-color var(--ha-animation-duration-normal)
|
||||
ease-in-out;
|
||||
}
|
||||
.base {
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
:host([state="visible"]) {
|
||||
background-color: var(--ha-color-fill-success-quiet-resting);
|
||||
--visibility-status-color: var(--ha-color-on-success-normal);
|
||||
}
|
||||
:host([state="hidden"]) {
|
||||
background-color: var(--ha-color-fill-warning-quiet-resting);
|
||||
--visibility-status-color: var(--ha-color-on-warning-normal);
|
||||
}
|
||||
:host([state="invalid"]) {
|
||||
background-color: var(--ha-color-fill-danger-quiet-resting);
|
||||
--visibility-status-color: var(--ha-color-on-danger-normal);
|
||||
}
|
||||
.start {
|
||||
align-self: start;
|
||||
}
|
||||
.start ha-svg-icon {
|
||||
color: var(--visibility-status-color);
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
.headline {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
white-space: normal;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-visibility-status": HaVisibilityStatus;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import { any, array, assert, assign, object, optional } from "superstruct";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tab-group";
|
||||
@@ -21,6 +20,7 @@ import "../card-editor/hui-card-element-editor";
|
||||
import type { HuiCardElementEditor } from "../card-editor/hui-card-element-editor";
|
||||
import "../card-editor/hui-card-picker";
|
||||
import "../conditions/ha-card-conditions-editor";
|
||||
import "../conditions/ha-visibility-status";
|
||||
import "../hui-element-editor";
|
||||
import type { ConfigChangedEvent } from "../hui-element-editor";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
@@ -147,11 +147,10 @@ export class HuiConditionalCardEditor
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.explanation"
|
||||
)}
|
||||
</ha-alert>
|
||||
<ha-visibility-status
|
||||
.hass=${this.hass}
|
||||
.conditions=${this._config.conditions ?? []}
|
||||
></ha-visibility-status>
|
||||
<ha-card-conditions-editor
|
||||
.hass=${this.hass}
|
||||
.conditions=${this._config.conditions}
|
||||
@@ -246,9 +245,9 @@ export class HuiConditionalCardEditor
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-top: 12px;
|
||||
ha-visibility-status {
|
||||
margin-top: var(--ha-space-3);
|
||||
margin-bottom: var(--ha-space-3);
|
||||
}
|
||||
.card {
|
||||
margin-top: 8px;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { Condition } from "../../common/validate-condition";
|
||||
import "../conditions/ha-card-conditions-editor";
|
||||
import "../conditions/ha-visibility-status";
|
||||
|
||||
@customElement("hui-section-visibility-editor")
|
||||
export class HuiDialogEditSection extends LitElement {
|
||||
@@ -16,11 +16,10 @@ export class HuiDialogEditSection extends LitElement {
|
||||
render() {
|
||||
const conditions = this.config.visibility ?? [];
|
||||
return html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_section.visibility.explanation`
|
||||
)}
|
||||
</ha-alert>
|
||||
<ha-visibility-status
|
||||
.hass=${this.hass}
|
||||
.conditions=${conditions}
|
||||
></ha-visibility-status>
|
||||
<ha-card-conditions-editor
|
||||
.hass=${this.hass}
|
||||
.conditions=${conditions}
|
||||
@@ -30,6 +29,12 @@ export class HuiDialogEditSection extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-visibility-status {
|
||||
margin-bottom: var(--ha-space-3);
|
||||
}
|
||||
`;
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const conditions = ev.detail.value as Condition[];
|
||||
|
||||
@@ -22,7 +22,7 @@ import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-slider";
|
||||
import { isUnavailableState } from "../../../data/entity/entity";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type {
|
||||
ControlButton,
|
||||
MediaPlayerEntity,
|
||||
@@ -195,7 +195,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
|
||||
<div class="controls">
|
||||
${supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_ON) &&
|
||||
(!stateActive(stateObj) || assumedState) &&
|
||||
!isUnavailableState(entityState)
|
||||
entityState !== UNAVAILABLE
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${assumedState ? mdiPowerOn : mdiPower}
|
||||
@@ -209,7 +209,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
|
||||
(stateActive(stateObj) ||
|
||||
assumedState ||
|
||||
!supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_ON) ||
|
||||
isUnavailableState(entityState))
|
||||
entityState === UNAVAILABLE)
|
||||
? buttons
|
||||
: ""}
|
||||
${supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_OFF) &&
|
||||
|
||||
@@ -61,10 +61,11 @@ export const securityEntityFilters: EntityFilter[] = [
|
||||
],
|
||||
entity_category: "none",
|
||||
},
|
||||
// We also want the tamper sensors when they are diagnostic
|
||||
// We also want the tamper and moisture sensors when they are diagnostic
|
||||
// (some integrations, e.g. homee, mark water leak alarms as diagnostic)
|
||||
{
|
||||
domain: "binary_sensor",
|
||||
device_class: ["tamper"],
|
||||
device_class: ["moisture", "tamper"],
|
||||
entity_category: "diagnostic",
|
||||
},
|
||||
];
|
||||
|
||||
+35
-14
@@ -1751,7 +1751,7 @@
|
||||
"no_areas_text_non_admin": "Ask an administrator to map your vacuum's segments to areas.",
|
||||
"configure_area_mapping": "Configure area mapping",
|
||||
"configure": "Configure",
|
||||
"clean_areas_order_hint": "Cleaning order may not be supported by your vacuum.",
|
||||
"clean_areas_order_hint": "The order in which areas are cleaned may not be supported by your vacuum.",
|
||||
"other_areas": "Other areas"
|
||||
},
|
||||
"person": {
|
||||
@@ -4115,6 +4115,7 @@
|
||||
"energy_from_helper": "Pick a sensor which measures grid import in either of {unit}.",
|
||||
"energy_to_grid": "Energy exported to grid",
|
||||
"energy_to_helper": "Pick a sensor which measures grid export in either of {unit}.",
|
||||
"display_name": "[%key:ui::panel::config::energy::device_consumption::dialog::display_name%]",
|
||||
"import_cost": "Cost tracking",
|
||||
"import_cost_para": "Select how Home Assistant should keep track of the costs of the imported energy.",
|
||||
"no_cost_tracking": "Do not track costs",
|
||||
@@ -4172,6 +4173,7 @@
|
||||
"dialog": {
|
||||
"header": "Configure solar panels",
|
||||
"entity_para": "Pick a sensor which measures solar production in either of {unit}.",
|
||||
"display_name": "[%key:ui::panel::config::energy::device_consumption::dialog::display_name%]",
|
||||
"solar_production_energy": "Solar production energy",
|
||||
"solar_production_power": "Solar production power",
|
||||
"solar_production_forecast": "Solar production forecast",
|
||||
@@ -4195,6 +4197,7 @@
|
||||
"energy_helper_out": "Pick a sensor that measures the electricity flowing out of the battery in either of {unit}.",
|
||||
"energy_into_battery": "Energy charged into the battery",
|
||||
"energy_out_of_battery": "Energy discharged from the battery",
|
||||
"display_name": "[%key:ui::panel::config::energy::device_consumption::dialog::display_name%]",
|
||||
"state_of_charge": "Battery state of charge sensor",
|
||||
"state_of_charge_helper": "Sensor reporting battery state of charge as %.",
|
||||
"power": "Battery power",
|
||||
@@ -8570,7 +8573,10 @@
|
||||
"total_usage": "+{num} kWh",
|
||||
"combined_from_grid": "Combined from grid",
|
||||
"consumed_solar": "Consumed solar",
|
||||
"consumed_battery": "Consumed battery"
|
||||
"consumed_battery": "Consumed battery",
|
||||
"named_battery_charged": "[%key:ui::panel::lovelace::cards::energy::energy_sources_table::named_battery_charged%]",
|
||||
"named_grid_consumed": "Consumed {name}",
|
||||
"named_grid_exported": "[%key:ui::panel::lovelace::cards::energy::energy_sources_table::named_grid_exported%]"
|
||||
},
|
||||
"energy_sources_table": {
|
||||
"grid_total": "Grid total",
|
||||
@@ -8583,7 +8589,11 @@
|
||||
"previous_energy": "Previous usage",
|
||||
"previous_cost": "Previous cost",
|
||||
"battery_total": "Battery total",
|
||||
"total_costs": "Total costs"
|
||||
"total_costs": "Total costs",
|
||||
"named_battery_charged": "{name} charged",
|
||||
"named_battery_discharged": "{name} discharged",
|
||||
"named_grid_imported": "{name} imported",
|
||||
"named_grid_exported": "{name} exported"
|
||||
},
|
||||
"energy_solar_graph": {
|
||||
"production": "Production {name}",
|
||||
@@ -8929,9 +8939,6 @@
|
||||
"tab_visibility": "Visibility",
|
||||
"tab_layout": "Layout",
|
||||
"paste_condition": "Paste condition",
|
||||
"visibility": {
|
||||
"explanation": "The card will be shown when ALL conditions below are fulfilled. If no conditions are set, the card will always be shown."
|
||||
},
|
||||
"layout": {
|
||||
"full_width": "Full width",
|
||||
"full_width_helper": "Take up the full width of the section whatever its size",
|
||||
@@ -8962,10 +8969,7 @@
|
||||
"cut": "[%key:ui::panel::lovelace::editor::edit_card::cut%]",
|
||||
"duplicate": "[%key:ui::panel::lovelace::editor::edit_card::duplicate%]",
|
||||
"tab_config": "[%key:ui::panel::lovelace::editor::edit_card::tab_config%]",
|
||||
"tab_visibility": "[%key:ui::panel::lovelace::editor::edit_card::tab_visibility%]",
|
||||
"visibility": {
|
||||
"explanation": "The badge will be shown when ALL conditions below are fulfilled. If no conditions are set, the badge will always be shown."
|
||||
}
|
||||
"tab_visibility": "[%key:ui::panel::lovelace::editor::edit_card::tab_visibility%]"
|
||||
},
|
||||
"suggest_badge": {
|
||||
"header": "[%key:ui::panel::lovelace::editor::suggest_card::header%]",
|
||||
@@ -9037,9 +9041,6 @@
|
||||
"background_opacity": "Opacity",
|
||||
"theme": "Theme",
|
||||
"theme_helper": "Apply a specific theme to this section, overriding the view theme"
|
||||
},
|
||||
"visibility": {
|
||||
"explanation": "The section will be shown when ALL conditions below are fulfilled. If no conditions are set, the section will always be shown."
|
||||
}
|
||||
},
|
||||
"suggest_card": {
|
||||
@@ -9081,11 +9082,31 @@
|
||||
}
|
||||
},
|
||||
"condition-editor": {
|
||||
"explanation": "The card will be shown when ALL conditions below are fulfilled.",
|
||||
"add": "Add condition",
|
||||
"test": "[%key:ui::panel::config::automation::editor::conditions::test%]",
|
||||
"testing_pass": "[%key:ui::panel::config::automation::editor::conditions::testing_pass%]",
|
||||
"testing_error": "[%key:ui::panel::config::automation::editor::conditions::testing_error%]",
|
||||
"live_test_state": {
|
||||
"pass": "[%key:ui::panel::config::automation::editor::conditions::testing_pass%]",
|
||||
"fail": "[%key:ui::panel::config::automation::editor::conditions::testing_error%]",
|
||||
"invalid": "[%key:ui::panel::config::automation::editor::conditions::live_test_state::invalid%]",
|
||||
"unknown": "[%key:ui::panel::config::automation::editor::conditions::live_test_state::unknown%]"
|
||||
},
|
||||
"visibility_status": {
|
||||
"visible": {
|
||||
"headline": "Current visibility: Visible",
|
||||
"supporting": "All visibility conditions are met",
|
||||
"supporting_empty": "No visibility conditions are set"
|
||||
},
|
||||
"hidden": {
|
||||
"headline": "Current visibility: Hidden",
|
||||
"supporting": "Not all visibility conditions are met"
|
||||
},
|
||||
"invalid": {
|
||||
"headline": "Visibility status unknown",
|
||||
"supporting": "One or more conditions have an invalid configuration"
|
||||
}
|
||||
},
|
||||
"invalid_config_title": "Invalid configuration",
|
||||
"invalid_config_text": "The condition cannot be tested because the configuration is not valid.",
|
||||
"condition": {
|
||||
|
||||
Reference in New Issue
Block a user