Compare commits

...

13 Commits

Author SHA1 Message Date
Petar Petrov 190ef6bb87 Show diagnostic moisture binary sensors on security dashboard 2026-05-19 10:27:23 +03:00
Jan-Philipp Benecke 91b6a4c4b6 Migrate energy sources table and drop mwc data table dependency (#52097)
* Migrate energy sources table and drop mwc data table dependency

* Address review comments

* Address review comments
2026-05-19 09:58:18 +03:00
karwosts 643cc4ca7d Make energy electric sources nameable (#52051)
Make electric sources nameable

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-05-19 06:37:49 +00:00
renovate[bot] 9ef71e6cf4 Update tsparticles to v4.0.1 (#52095)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-19 07:18:29 +02:00
renovate[bot] bface72af7 Lock file maintenance (#52096)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-19 07:18:12 +02:00
Paul Bottein 90028b2e22 Clarify cleaning order hint in vacuum more info (#52087) 2026-05-18 22:29:36 +02:00
Ben Hamilton (Ben Gertzfield) 914c48abd5 Allow media player source card feature when list is empty (#52094) 2026-05-18 19:05:12 +00:00
renovate[bot] 79c082acde Update dependency eslint to v10.4.0 (#52093)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-18 20:36:03 +02:00
Marcin Bauer 4728eb7231 Remove arrow icon from continue on error indicator (#52092)
The arrow-right icon next to the alert icon was decorative noise.
With automation comments (#52090) adding yet another icon, simplify
to a single mdiAlertCircleCheck indicator.

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-18 17:28:14 +03:00
renovate[bot] d02b92bd32 Update dependency @tsparticles/engine to v4 (#52091)
* Update dependency @tsparticles/engine to v4

* Bump @tsparticles/preset-links to v4 to match engine

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-05-18 14:15:54 +00:00
Wendelin 98525d23e6 Lovelace condition live test (#52027)
* Add lovelace condition live test

* Add live card status

* Add empty text
2026-05-18 15:01:56 +02:00
Petar Petrov ec98b21276 Highlight problematic devices in Energy Dashboard list (#52088) 2026-05-18 10:18:27 +01:00
Paulus Schoutsen defad3beca Treat media player unknown state like off instead of unavailable (#52080)
* Show both power buttons for assumed-state media players when unknown

Media players with assumed state report an unknown state when their
actual power state can't be determined. In that case the entity row and
more info should still expose both turn on and turn off controls so the
user can operate the device.

https://claude.ai/code/session_01JyZojNPCCY65HmRVQaASkG

* Treat media player unknown state like off instead of unavailable

The media player controls lumped the "unknown" state in with
"unavailable" and hid all controls. An unknown state is closer to "off":
the device exists but its power state isn't reported, which is common
for assumed-state players. Only "unavailable" should hide the controls,
so an unknown-state player now shows the turn on button (and both power
buttons when it has an assumed state) in the entity row and more info.

https://claude.ai/code/session_01JyZojNPCCY65HmRVQaASkG

* Adjust comments and variable placement for media player state check

https://claude.ai/code/session_01JyZojNPCCY65HmRVQaASkG

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-18 09:58:01 +03:00
31 changed files with 1702 additions and 1148 deletions
+3 -4
View File
@@ -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;
}
}
+3
View File
@@ -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";
+3 -2
View File
@@ -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
View File
@@ -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": {
+683 -855
View File
File diff suppressed because it is too large Load Diff