mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-23 00:31:40 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b72791a9e2 | |||
| a3c0e8d519 | |||
| 19fcb9d2f7 | |||
| cbd90884ee | |||
| fce1938f38 | |||
| 24821d6f1b | |||
| 677d64c915 | |||
| fe06772a73 | |||
| 170f8c371a | |||
| 12841b5ff7 | |||
| 5393b05636 | |||
| a384e2dbd6 | |||
| fd4936e547 | |||
| 44d02420ae |
@@ -0,0 +1,75 @@
|
||||
import type { DemoTrace } from "./types";
|
||||
|
||||
export const notTriggeredTrace: DemoTrace = {
|
||||
trace: {
|
||||
last_step: "trigger/0",
|
||||
run_id: "788767ce152d3d4475134bf1107986d4",
|
||||
state: "stopped",
|
||||
script_execution: "not_triggered",
|
||||
not_triggered: true,
|
||||
timestamp: {
|
||||
start: "2021-03-25T04:36:51.223337+00:00",
|
||||
finish: "2021-03-25T04:36:51.223341+00:00",
|
||||
},
|
||||
// Not-triggered traces have no trigger description.
|
||||
trigger: null,
|
||||
domain: "automation",
|
||||
item_id: "1781703842452",
|
||||
trace: {
|
||||
"trigger/0": [
|
||||
{
|
||||
path: "trigger/0",
|
||||
timestamp: "2021-03-25T04:36:51.223340+00:00",
|
||||
changed_variables: {
|
||||
trigger: {
|
||||
id: "0",
|
||||
idx: "0",
|
||||
alias: null,
|
||||
platform: "light.turned_on",
|
||||
},
|
||||
},
|
||||
result: {
|
||||
reason: "new_state_not_a_match",
|
||||
data: {
|
||||
entity_id: "light.bed_light",
|
||||
to_state: "off",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
config: {
|
||||
id: "1781703842452",
|
||||
alias: "Light Turned On Notification",
|
||||
description: "Send a notification when a specific light is turned on.",
|
||||
triggers: [
|
||||
{
|
||||
trigger: "light.turned_on",
|
||||
target: {
|
||||
floor_id: "test",
|
||||
},
|
||||
options: {
|
||||
for: "00:00:00",
|
||||
behavior: "each",
|
||||
},
|
||||
},
|
||||
],
|
||||
conditions: [],
|
||||
actions: [
|
||||
{
|
||||
action: "notify.notify",
|
||||
data: {
|
||||
message: "A light was turned on.",
|
||||
},
|
||||
},
|
||||
],
|
||||
mode: "single",
|
||||
},
|
||||
context: {
|
||||
id: "01KVAX7CG7XBDYGJYAGA4XJHGX",
|
||||
parent_id: "01KVAX7CG631JRX4H3JS5JJ11Q",
|
||||
user_id: null,
|
||||
},
|
||||
},
|
||||
logbookEntries: [],
|
||||
};
|
||||
@@ -24,6 +24,33 @@ const traces: DemoTrace[] = [
|
||||
error: 'Variable "beer" cannot be None',
|
||||
}),
|
||||
mockDemoTrace({ state: "stopped", script_execution: "cancelled" }),
|
||||
mockDemoTrace({
|
||||
state: "stopped",
|
||||
script_execution: "not_triggered",
|
||||
not_triggered: true,
|
||||
// Not-triggered traces have no trigger description.
|
||||
trigger: null,
|
||||
trace: {
|
||||
"trigger/0": [
|
||||
{
|
||||
path: "trigger/0",
|
||||
changed_variables: {
|
||||
trigger: {
|
||||
id: "0",
|
||||
idx: "0",
|
||||
alias: null,
|
||||
platform: "light.turned_on",
|
||||
},
|
||||
},
|
||||
result: {
|
||||
reason: "new_state_not_a_match",
|
||||
data: { entity_id: "light.bed_light", to_state: "off" },
|
||||
},
|
||||
timestamp: "2021-03-25T04:36:51.223693+00:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
@customElement("demo-automation-trace-timeline")
|
||||
|
||||
@@ -2,17 +2,20 @@
|
||||
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, queryAll, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/trace/ha-trace-path-details";
|
||||
import type { HatScriptGraph } from "../../../../src/components/trace/hat-script-graph";
|
||||
import "../../../../src/components/trace/hat-script-graph";
|
||||
import "../../../../src/components/trace/hat-trace-timeline";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { basicTrace } from "../../data/traces/basic_trace";
|
||||
import { motionLightTrace } from "../../data/traces/motion-light-trace";
|
||||
import { notTriggeredTrace } from "../../data/traces/not-triggered-trace";
|
||||
import type { DemoTrace } from "../../data/traces/types";
|
||||
|
||||
const traces: DemoTrace[] = [basicTrace, motionLightTrace];
|
||||
const traces: DemoTrace[] = [basicTrace, motionLightTrace, notTriggeredTrace];
|
||||
|
||||
@customElement("demo-automation-trace")
|
||||
export class DemoAutomationTrace extends LitElement {
|
||||
@@ -20,18 +23,25 @@ export class DemoAutomationTrace extends LitElement {
|
||||
|
||||
@state() private _selected = {};
|
||||
|
||||
@queryAll("hat-script-graph") private _graphs!: NodeListOf<HatScriptGraph>;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
${traces.map(
|
||||
(trace, idx) => html`
|
||||
${traces.map((trace, idx) => {
|
||||
const graph = this._graphs?.[idx];
|
||||
const selectedPath = this._selected[idx];
|
||||
const selectedNode = selectedPath
|
||||
? graph?.renderedNodes[selectedPath]
|
||||
: undefined;
|
||||
return html`
|
||||
<ha-card .header=${trace.trace.config.alias}>
|
||||
<div class="card-content">
|
||||
<hat-script-graph
|
||||
.trace=${trace.trace}
|
||||
.selected=${this._selected[idx]}
|
||||
.selected=${selectedPath}
|
||||
@graph-node-selected=${this._handleGraphNodeSelected}
|
||||
.sampleIdx=${idx}
|
||||
></hat-script-graph>
|
||||
@@ -40,15 +50,25 @@ export class DemoAutomationTrace extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.trace=${trace.trace}
|
||||
.logbookEntries=${trace.logbookEntries}
|
||||
.selectedPath=${this._selected[idx]}
|
||||
.selectedPath=${selectedPath}
|
||||
@value-changed=${this._handleTimelineValueChanged}
|
||||
.sampleIdx=${idx}
|
||||
></hat-trace-timeline>
|
||||
${selectedNode && graph
|
||||
? html`<ha-trace-path-details
|
||||
.hass=${this.hass}
|
||||
.trace=${trace.trace}
|
||||
.selected=${selectedNode}
|
||||
.logbookEntries=${trace.logbookEntries}
|
||||
.trackedNodes=${graph.trackedNodes}
|
||||
.renderedNodes=${graph.renderedNodes}
|
||||
></ha-trace-path-details>`
|
||||
: nothing}
|
||||
<button @click=${() => console.log(trace)}>Log trace</button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
})}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -353,7 +353,6 @@ export class DemoEntityState extends LitElement {
|
||||
title: "Icon",
|
||||
template: (entry) => html`
|
||||
<state-badge
|
||||
.hass=${hass}
|
||||
.stateObj=${entry.stateObj}
|
||||
.stateColor=${true}
|
||||
></state-badge>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
|
||||
import type { RelatedResult } from "../../data/search";
|
||||
import type { ItemType, RelatedResult } from "../../data/search";
|
||||
|
||||
export interface RelatedIdSets {
|
||||
areas: Set<string>;
|
||||
@@ -8,14 +8,30 @@ export interface RelatedIdSets {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a set of related IDs for a given related result.
|
||||
* Build a set of related IDs, merging in the current (queried) item.
|
||||
* `search/related` does not echo the queried item back, but it is the closest
|
||||
* related item (e.g. a card editor's own entity), so it is merged into the
|
||||
* matching group when it is an area, device, or entity.
|
||||
* @param related - The related result to build the sets from.
|
||||
* @returns The related ID sets.
|
||||
* @param current - The queried item to merge in.
|
||||
* @returns The related ID sets, including the current item.
|
||||
*/
|
||||
export const buildRelatedIdSets = (related?: RelatedResult): RelatedIdSets => ({
|
||||
areas: new Set(related?.area || []),
|
||||
devices: new Set(related?.device || []),
|
||||
entities: new Set(related?.entity || []),
|
||||
export const buildRelatedIdSets = (
|
||||
related?: RelatedResult,
|
||||
current?: { itemType: ItemType; itemId: string }
|
||||
): RelatedIdSets => ({
|
||||
areas: new Set([
|
||||
...(related?.area || []),
|
||||
...(current?.itemType === "area" ? [current.itemId] : []),
|
||||
]),
|
||||
devices: new Set([
|
||||
...(related?.device || []),
|
||||
...(current?.itemType === "device" ? [current.itemId] : []),
|
||||
]),
|
||||
entities: new Set([
|
||||
...(related?.entity || []),
|
||||
...(current?.itemType === "entity" ? [current.itemId] : []),
|
||||
]),
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import "./ha-progress-button";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { apiContext } from "../../data/context";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { Appearance } from "../ha-button";
|
||||
|
||||
@customElement("ha-call-service-button")
|
||||
class HaCallServiceButton extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@@ -56,7 +58,7 @@ class HaCallServiceButton extends LitElement {
|
||||
this.shadowRoot!.querySelector("ha-progress-button")!;
|
||||
|
||||
try {
|
||||
await this.hass.callService(
|
||||
await this._api.callService(
|
||||
this.domain,
|
||||
this.service,
|
||||
this.data,
|
||||
|
||||
@@ -445,6 +445,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
private _formatYAxisLabel = (value: number) => {
|
||||
const label = formatNumber(value, this.hass.locale, {
|
||||
minimumFractionDigits: value === 0 ? 0 : this._yAxisFractionDigits,
|
||||
maximumFractionDigits: this._yAxisFractionDigits,
|
||||
});
|
||||
const width = measureTextWidth(label, 12) + 5;
|
||||
|
||||
@@ -552,6 +552,7 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
private _formatYAxisLabel = (value: number) =>
|
||||
formatNumber(value, this.hass.locale, {
|
||||
minimumFractionDigits: value === 0 ? 0 : this._yAxisFractionDigits,
|
||||
maximumFractionDigits: this._yAxisFractionDigits,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiPlus, mdiShape } from "@mdi/js";
|
||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
@@ -6,10 +7,14 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeEntityPickerDisplay } from "../../common/entity/compute_entity_name_display";
|
||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||
import type { RelatedIdSets } from "../../common/search/related-context";
|
||||
import { relatedContext } from "../../data/context";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||
import {
|
||||
entityComboBoxKeys,
|
||||
getEntities,
|
||||
markEntitiesRelated,
|
||||
sortEntitiesByRelatedRank,
|
||||
type EntityComboBoxItem,
|
||||
} from "../../data/entity/entity_picker";
|
||||
import { domainToName } from "../../data/integration";
|
||||
@@ -131,6 +136,20 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
@state() private _pendingEntityId?: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: relatedContext, subscribe: true })
|
||||
private _relatedIdSets?: RelatedIdSets;
|
||||
|
||||
private get _hasRelatedContext(): boolean {
|
||||
const related = this._relatedIdSets;
|
||||
return (
|
||||
!!related &&
|
||||
(related.entities.size > 0 ||
|
||||
related.devices.size > 0 ||
|
||||
related.areas.size > 0)
|
||||
);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (
|
||||
this._pendingEntityId &&
|
||||
@@ -161,11 +180,7 @@ export class HaEntityPicker extends LitElement {
|
||||
: undefined;
|
||||
if (stateObj) {
|
||||
return html`
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
<state-badge slot="start" .stateObj=${stateObj}></state-badge>
|
||||
`;
|
||||
}
|
||||
if (extraOption.icon_path) {
|
||||
@@ -216,11 +231,7 @@ export class HaEntityPicker extends LitElement {
|
||||
);
|
||||
|
||||
return html`
|
||||
<state-badge
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
slot="start"
|
||||
></state-badge>
|
||||
<state-badge .stateObj=${stateObj} slot="start"></state-badge>
|
||||
<span slot="headline">${primary}</span>
|
||||
<span slot="supporting-text">${secondary}</span>
|
||||
`;
|
||||
@@ -250,7 +261,6 @@ export class HaEntityPicker extends LitElement {
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${item.stateObj}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`}
|
||||
<span slot="headline">${item.primary}</span>
|
||||
@@ -333,8 +343,22 @@ export class HaEntityPicker extends LitElement {
|
||||
})
|
||||
);
|
||||
|
||||
private _sortByRelatedContext = memoizeOne(
|
||||
(
|
||||
items: EntityComboBoxItem[],
|
||||
related: RelatedIdSets,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
language: string
|
||||
): EntityComboBoxItem[] =>
|
||||
sortEntitiesByRelatedRank(
|
||||
markEntitiesRelated(items, related, entities, devices),
|
||||
language
|
||||
)
|
||||
);
|
||||
|
||||
private _getItems = () => {
|
||||
const items = this._getEntitiesMemoized(
|
||||
const entityItems = this._getEntitiesMemoized(
|
||||
this.hass,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
@@ -345,14 +369,23 @@ export class HaEntityPicker extends LitElement {
|
||||
this.excludeEntities,
|
||||
this.value
|
||||
);
|
||||
const sortedItems = this._hasRelatedContext
|
||||
? this._sortByRelatedContext(
|
||||
entityItems,
|
||||
this._relatedIdSets!,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.locale.language
|
||||
)
|
||||
: entityItems;
|
||||
if (this.extraOptions?.length) {
|
||||
const resolvedExtras = this.extraOptions.map((opt) => ({
|
||||
...opt,
|
||||
stateObj: opt.entity_id ? this.hass.states[opt.entity_id] : undefined,
|
||||
}));
|
||||
return [...resolvedExtras, ...items];
|
||||
return [...resolvedExtras, ...sortedItems];
|
||||
}
|
||||
return items;
|
||||
return sortedItems;
|
||||
};
|
||||
|
||||
private _shouldHideClearIcon() {
|
||||
@@ -384,6 +417,7 @@ export class HaEntityPicker extends LitElement {
|
||||
.searchFn=${this._searchFn}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.searchKeys=${entityComboBoxKeys}
|
||||
.noSort=${this._hasRelatedContext}
|
||||
use-top-label
|
||||
.addButtonLabel=${this.addButton
|
||||
? (this.addButtonLabel ??
|
||||
@@ -402,17 +436,23 @@ export class HaEntityPicker extends LitElement {
|
||||
search,
|
||||
filteredItems
|
||||
) => {
|
||||
// Float related items to the top by closeness, keeping search relevance
|
||||
// order within each tier.
|
||||
const items = this._hasRelatedContext
|
||||
? sortEntitiesByRelatedRank(filteredItems)
|
||||
: filteredItems;
|
||||
|
||||
// If there is exact match for entity id, put it first
|
||||
const index = filteredItems.findIndex(
|
||||
const index = items.findIndex(
|
||||
(item) => item.stateObj?.entity_id === search
|
||||
);
|
||||
if (index === -1) {
|
||||
return filteredItems;
|
||||
return items;
|
||||
}
|
||||
|
||||
const [exactMatch] = filteredItems.splice(index, 1);
|
||||
filteredItems.unshift(exactMatch);
|
||||
return filteredItems;
|
||||
const [exactMatch] = items.splice(index, 1);
|
||||
items.unshift(exactMatch);
|
||||
return items;
|
||||
};
|
||||
|
||||
public async open() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiFlash, mdiFlashOff } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
@@ -6,9 +7,9 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { STATES_OFF } from "../../common/const";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { apiContext } from "../../data/context";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import { forwardHaptic } from "../../data/haptics";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-formfield";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-switch";
|
||||
@@ -29,8 +30,8 @@ const isOn = (stateObj?: HassEntity) =>
|
||||
|
||||
@customElement("ha-entity-toggle")
|
||||
export class HaEntityToggle extends LitElement {
|
||||
// hass is not a property so that we only re-render on stateObj changes
|
||||
public hass?: HomeAssistant;
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api?: ContextType<typeof apiContext>;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@@ -118,7 +119,7 @@ export class HaEntityToggle extends LitElement {
|
||||
// result in the entity to be turned on. Since the state is not changing,
|
||||
// the resync is not called automatic.
|
||||
private async _callService(turnOn): Promise<void> {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
if (!this._api || !this.stateObj) {
|
||||
return;
|
||||
}
|
||||
forwardHaptic(this, "light");
|
||||
@@ -149,7 +150,7 @@ export class HaEntityToggle extends LitElement {
|
||||
this._isOn = turnOn;
|
||||
|
||||
try {
|
||||
await this.hass.callService(serviceDomain, service, {
|
||||
await this._api.callService(serviceDomain, service, {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { mdiAlert } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
@@ -6,14 +8,19 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { arrayLiteralIncludes } from "../../common/array/literal-includes";
|
||||
import secondsToDuration from "../../common/datetime/seconds_to_duration";
|
||||
import {
|
||||
consumeEntityRegistryEntry,
|
||||
consumeLocalize,
|
||||
} from "../../common/decorators/consume-context-entry";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { unitFromParts, valueFromParts } from "../../common/entity/value_parts";
|
||||
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { formattersContext } from "../../data/context";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import { timerTimeRemaining } from "../../data/timer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-label-badge";
|
||||
import "../ha-state-icon";
|
||||
|
||||
@@ -41,7 +48,15 @@ const getTruncatedKey = (domainKey: string, stateKey: string) => {
|
||||
|
||||
@customElement("ha-state-label-badge")
|
||||
export class HaStateLabelBadge extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
private _formatters?: ContextType<typeof formattersContext>;
|
||||
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consumeEntityRegistryEntry({ entityIdPath: ["state", "entity_id"] })
|
||||
private _entry?: EntityRegistryDisplayEntry;
|
||||
|
||||
@property({ attribute: false }) public state?: HassEntity;
|
||||
|
||||
@@ -78,10 +93,8 @@ export class HaStateLabelBadge extends LitElement {
|
||||
return html`
|
||||
<ha-label-badge
|
||||
class="warning"
|
||||
label=${this.hass!.localize("state_badge.default.error")}
|
||||
description=${this.hass!.localize(
|
||||
"state_badge.default.entity_not_found"
|
||||
)}
|
||||
label=${this._localize("state_badge.default.error")}
|
||||
description=${this._localize("state_badge.default.entity_not_found")}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiAlert}></ha-svg-icon>
|
||||
</ha-label-badge>
|
||||
@@ -95,7 +108,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
// 4. Icon determined via entity state
|
||||
// 5. Value string as fallback
|
||||
const domain = computeStateDomain(entityState);
|
||||
const entry = this.hass?.entities[entityState.entity_id];
|
||||
const entry = this._entry;
|
||||
|
||||
const showIcon =
|
||||
this.icon || this._computeShowIcon(domain, entityState, entry);
|
||||
@@ -175,7 +188,12 @@ export class HaStateLabelBadge extends LitElement {
|
||||
if (entityState.state === UNAVAILABLE || entityState.state === UNKNOWN) {
|
||||
return "—";
|
||||
}
|
||||
return valueFromParts(this.hass!.formatEntityStateToParts(entityState));
|
||||
if (!this._formatters) {
|
||||
return null;
|
||||
}
|
||||
return valueFromParts(
|
||||
this._formatters.formatEntityStateToParts(entityState)
|
||||
);
|
||||
}
|
||||
|
||||
private _computeShowIcon(
|
||||
@@ -210,11 +228,11 @@ export class HaStateLabelBadge extends LitElement {
|
||||
) {
|
||||
// For unavailable states or certain domains, use a special translation that is truncated to fit within the badge label
|
||||
if (entityState.state === UNAVAILABLE || entityState.state === UNKNOWN) {
|
||||
return this.hass!.localize(`state_badge.default.${entityState.state}`);
|
||||
return this._localize(`state_badge.default.${entityState.state}`);
|
||||
}
|
||||
const domainStateKey = getTruncatedKey(domain, entityState.state);
|
||||
if (domainStateKey) {
|
||||
return this.hass!.localize(`state_badge.${domainStateKey}`);
|
||||
return this._localize(`state_badge.${domainStateKey}`);
|
||||
}
|
||||
// Person and device tracker state can be zone name
|
||||
if (domain === "person" || domain === "device_tracker") {
|
||||
@@ -223,8 +241,12 @@ export class HaStateLabelBadge extends LitElement {
|
||||
if (domain === "timer") {
|
||||
return secondsToDuration(_timerTimeRemaining);
|
||||
}
|
||||
if (!this._formatters) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
unitFromParts(this.hass!.formatEntityStateToParts(entityState)) || null
|
||||
unitFromParts(this._formatters.formatEntityStateToParts(entityState)) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -343,11 +343,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
return html`
|
||||
${item.stateObj
|
||||
? html`
|
||||
<state-badge
|
||||
.hass=${this.hass}
|
||||
.stateObj=${item.stateObj}
|
||||
slot="start"
|
||||
></state-badge>
|
||||
<state-badge .stateObj=${item.stateObj} slot="start"></state-badge>
|
||||
`
|
||||
: item.icon_path
|
||||
? html`
|
||||
@@ -488,7 +484,6 @@ export class HaStatisticPicker extends LitElement {
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${item.stateObj}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiAlert } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
@@ -14,13 +15,12 @@ import {
|
||||
import { iconColorCSS } from "../../common/style/icon_color_css";
|
||||
import { cameraUrlWithWidthHeight } from "../../data/camera";
|
||||
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { connectionContext } from "../../data/context";
|
||||
import { isBrandUrl } from "../../util/brands-url";
|
||||
import "../ha-state-icon";
|
||||
|
||||
@customElement("state-badge")
|
||||
export class StateBadge extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@property({ attribute: false }) public overrideIcon?: string;
|
||||
@@ -36,6 +36,10 @@ export class StateBadge extends LitElement {
|
||||
// @todo Consider reworking to eliminate need for attribute since it is manipulated internally
|
||||
@property({ type: Boolean, reflect: true }) public icon = true;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection?: ContextType<typeof connectionContext>;
|
||||
|
||||
@state() private _iconStyle: Record<string, string | undefined> = {};
|
||||
|
||||
connectedCallback(): void {
|
||||
@@ -106,14 +110,15 @@ export class StateBadge extends LitElement {
|
||||
></ha-state-icon>`;
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
if (
|
||||
!changedProps.has("stateObj") &&
|
||||
!changedProps.has("overrideImage") &&
|
||||
!changedProps.has("overrideIcon") &&
|
||||
!changedProps.has("stateColor") &&
|
||||
!changedProps.has("color")
|
||||
!changedProps.has("color") &&
|
||||
!changedProps.has("_connection")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -133,12 +138,10 @@ export class StateBadge extends LitElement {
|
||||
stateObj.attributes.entity_picture) &&
|
||||
!this.overrideIcon
|
||||
) {
|
||||
let imageUrl =
|
||||
let imageUrl = this._resolveImageUrl(
|
||||
stateObj.attributes.entity_picture_local ||
|
||||
stateObj.attributes.entity_picture;
|
||||
if (this.hass) {
|
||||
imageUrl = this.hass.hassUrl(imageUrl);
|
||||
}
|
||||
stateObj.attributes.entity_picture
|
||||
);
|
||||
if (domain === "camera") {
|
||||
imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80);
|
||||
}
|
||||
@@ -179,11 +182,7 @@ export class StateBadge extends LitElement {
|
||||
}
|
||||
}
|
||||
} else if (this.overrideImage) {
|
||||
let imageUrl = this.overrideImage;
|
||||
if (this.hass) {
|
||||
imageUrl = this.hass.hassUrl(imageUrl);
|
||||
}
|
||||
backgroundImage = `url(${imageUrl})`;
|
||||
backgroundImage = `url(${this._resolveImageUrl(this.overrideImage)})`;
|
||||
this.icon = false;
|
||||
}
|
||||
}
|
||||
@@ -192,6 +191,20 @@ export class StateBadge extends LitElement {
|
||||
this.style.backgroundImage = backgroundImage;
|
||||
}
|
||||
|
||||
// Sign the image URL via the connection context so brand images
|
||||
// (/api/brands/...) get their access token. Without a way to sign, a brands
|
||||
// request would be rejected (and logged/blocked by core), so skip it until
|
||||
// we can sign.
|
||||
private _resolveImageUrl(url: string | undefined): string {
|
||||
if (!url) {
|
||||
return "";
|
||||
}
|
||||
if (this._connection) {
|
||||
return this._connection.hassUrl(url);
|
||||
}
|
||||
return isBrandUrl(url) ? "" : url;
|
||||
}
|
||||
|
||||
protected getClass() {
|
||||
const cls = new Map(
|
||||
["has-no-radius", "has-media-image", "has-image"].map((_cls) => [
|
||||
|
||||
@@ -24,7 +24,6 @@ class StateInfo extends LitElement {
|
||||
const name = this.hass.formatEntityName(this.stateObj, { type: "entity" });
|
||||
|
||||
return html`<state-badge
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
.stateColor=${true}
|
||||
.color=${this.color}
|
||||
|
||||
@@ -173,6 +173,7 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
domainItems = multiTermSortedSearch(
|
||||
domainItems,
|
||||
searchString,
|
||||
this._domainSearchKeys,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
@@ -225,6 +226,7 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
entityItems = multiTermSortedSearch(
|
||||
entityItems,
|
||||
searchString,
|
||||
this._entitySearchKeys,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiDelete, mdiFileUpload } from "@mdi/js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { transform } from "../common/decorators/transform";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { internationalizationContext } from "../data/context";
|
||||
import type { FrontendLocaleData } from "../data/translation";
|
||||
import type { HomeAssistantInternationalization } from "../types";
|
||||
import { bytesToString } from "../util/bytes-to-string";
|
||||
import "./ha-button";
|
||||
import "./ha-icon-button";
|
||||
@@ -22,10 +27,17 @@ declare global {
|
||||
|
||||
@customElement("ha-file-upload")
|
||||
export class HaFileUpload extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@state() @consumeLocalize() private _localize?: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
|
||||
transformer: ({ locale }) => locale,
|
||||
})
|
||||
private _locale?: FrontendLocaleData;
|
||||
|
||||
@property() public accept!: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
@@ -80,7 +92,7 @@ export class HaFileUpload extends LitElement {
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
const localize = this.localize || this.hass!.localize;
|
||||
const localize = this.localize || this._localize!;
|
||||
return html`
|
||||
${this.uploading
|
||||
? html`<div class="container">
|
||||
@@ -95,8 +107,8 @@ export class HaFileUpload extends LitElement {
|
||||
>
|
||||
${this.progress
|
||||
? html`<div class="progress">
|
||||
${this.progress}${this.hass &&
|
||||
blankBeforePercent(this.hass!.locale)}%
|
||||
${this.progress}${this._locale &&
|
||||
blankBeforePercent(this._locale)}%
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDeviceNameDisplay } from "../common/entity/compute_device_name";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import {
|
||||
apiContext,
|
||||
devicesContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
} from "../data/context";
|
||||
import type { RelatedResult } from "../data/search";
|
||||
import { findRelated } from "../data/search";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
@@ -24,7 +32,24 @@ interface HaFilterDevicesItem extends HaListVirtualizedItem {
|
||||
|
||||
@customElement("ha-filter-devices")
|
||||
export class HaFilterDevices extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
@state()
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
@consume({ context: devicesContext, subscribe: true })
|
||||
@state()
|
||||
private _devicesReg!: ContextType<typeof devicesContext>;
|
||||
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@state()
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@@ -75,7 +100,7 @@ export class HaFilterDevices extends LitElement {
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this.hass.localize("ui.panel.config.devices.caption")}
|
||||
${this._localize("ui.panel.config.devices.caption")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
@@ -95,7 +120,13 @@ export class HaFilterDevices extends LitElement {
|
||||
</ha-input-search>
|
||||
<ha-list-selectable-virtualized
|
||||
multi
|
||||
.rows=${this._devices(this.hass.devices, this._filter || "")}
|
||||
.rows=${this._devices(
|
||||
this._devicesReg,
|
||||
this._filter || "",
|
||||
this._localize,
|
||||
this._states,
|
||||
this._i18n.locale.language
|
||||
)}
|
||||
.rowRenderer=${this._renderItem}
|
||||
@ha-list-item-selected=${this._handleAdded}
|
||||
@ha-list-item-deselected=${this._handleRemoved}
|
||||
@@ -121,13 +152,24 @@ export class HaFilterDevices extends LitElement {
|
||||
private _handleAdded(ev: CustomEvent<number>) {
|
||||
this.value = [
|
||||
...(this.value ?? []),
|
||||
this._devices(this.hass.devices, this._filter || "")[ev.detail].id,
|
||||
this._devices(
|
||||
this._devicesReg,
|
||||
this._filter || "",
|
||||
this._localize,
|
||||
this._states,
|
||||
this._i18n.locale.language
|
||||
)[ev.detail].id,
|
||||
];
|
||||
}
|
||||
|
||||
private _handleRemoved(ev: CustomEvent<number>) {
|
||||
const id = this._devices(this.hass.devices, this._filter || "")[ev.detail]
|
||||
.id;
|
||||
const id = this._devices(
|
||||
this._devicesReg,
|
||||
this._filter || "",
|
||||
this._localize,
|
||||
this._states,
|
||||
this._i18n.locale.language
|
||||
)[ev.detail].id;
|
||||
this.value = (this.value ?? []).filter((deviceId) => deviceId !== id);
|
||||
}
|
||||
|
||||
@@ -153,27 +195,24 @@ export class HaFilterDevices extends LitElement {
|
||||
|
||||
private _devices = memoizeOne(
|
||||
(
|
||||
devices: HomeAssistant["devices"],
|
||||
filter: string
|
||||
devices: ContextType<typeof devicesContext>,
|
||||
filter: string,
|
||||
localize: LocalizeFunc,
|
||||
states: ContextType<typeof statesContext>,
|
||||
language: string | undefined
|
||||
): HaFilterDevicesItem[] => {
|
||||
const values = Object.values(devices);
|
||||
return values
|
||||
.map((device) => ({
|
||||
id: device.id,
|
||||
interactive: true,
|
||||
name: computeDeviceNameDisplay(
|
||||
device,
|
||||
this.hass.localize,
|
||||
this.hass.states
|
||||
),
|
||||
name: computeDeviceNameDisplay(device, localize, states),
|
||||
}))
|
||||
.filter(
|
||||
({ name }) =>
|
||||
!filter || name.toLowerCase().includes(filter.toLowerCase())
|
||||
)
|
||||
.sort((a, b) =>
|
||||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||||
);
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language));
|
||||
}
|
||||
);
|
||||
|
||||
@@ -194,7 +233,7 @@ export class HaFilterDevices extends LitElement {
|
||||
for (const deviceId of this.value) {
|
||||
value.push(deviceId);
|
||||
if (this.type) {
|
||||
relatedPromises.push(findRelated(this.hass, "device", deviceId));
|
||||
relatedPromises.push(findRelated(this._api, "device", deviceId));
|
||||
}
|
||||
}
|
||||
const results = await Promise.all(relatedPromises);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
@@ -5,12 +6,14 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { internationalizationContext, statesContext } from "../data/context";
|
||||
import { domainToName } from "../data/integration";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-domain-icon";
|
||||
import "./ha-expansion-panel";
|
||||
@@ -20,7 +23,17 @@ import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@customElement("ha-filter-domains")
|
||||
export class HaFilterDomains extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
@state()
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@state()
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@@ -43,7 +56,7 @@ export class HaFilterDomains extends LitElement {
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this.hass.localize("ui.panel.config.domains.caption")}
|
||||
${this._localize("ui.panel.config.domains.caption")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
@@ -65,7 +78,13 @@ export class HaFilterDomains extends LitElement {
|
||||
multi
|
||||
>
|
||||
${repeat(
|
||||
this._domains(this.hass.states, this._filter, this.value),
|
||||
this._domains(
|
||||
this._states,
|
||||
this._localize,
|
||||
this._i18n.locale.language,
|
||||
this._filter,
|
||||
this.value
|
||||
),
|
||||
(i) => i,
|
||||
(domain) =>
|
||||
html`<ha-check-list-item
|
||||
@@ -78,7 +97,7 @@ export class HaFilterDomains extends LitElement {
|
||||
.domain=${domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
${domainToName(this.hass.localize, domain)}
|
||||
${domainToName(this._localize, domain)}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</ha-list> `
|
||||
@@ -87,26 +106,34 @@ export class HaFilterDomains extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _domains = memoizeOne((states, filter, _value) => {
|
||||
const domains = new Set<string>();
|
||||
Object.keys(states).forEach((entityId) => {
|
||||
domains.add(computeDomain(entityId));
|
||||
});
|
||||
private _domains = memoizeOne(
|
||||
(
|
||||
states: ContextType<typeof statesContext>,
|
||||
localize: LocalizeFunc,
|
||||
language: string | undefined,
|
||||
filter: string | undefined,
|
||||
_value
|
||||
) => {
|
||||
const domains = new Set<string>();
|
||||
Object.keys(states).forEach((entityId) => {
|
||||
domains.add(computeDomain(entityId));
|
||||
});
|
||||
|
||||
return Array.from(domains.values())
|
||||
.map((domain) => ({
|
||||
domain,
|
||||
name: domainToName(this.hass.localize, domain),
|
||||
}))
|
||||
.filter(
|
||||
(entry) =>
|
||||
!filter ||
|
||||
entry.domain.toLowerCase().includes(filter) ||
|
||||
entry.name.toLowerCase().includes(filter)
|
||||
)
|
||||
.sort((a, b) => stringCompare(a.name, b.name, this.hass.locale.language))
|
||||
.map((entry) => entry.domain);
|
||||
});
|
||||
return Array.from(domains.values())
|
||||
.map((domain) => ({
|
||||
domain,
|
||||
name: domainToName(localize, domain),
|
||||
}))
|
||||
.filter(
|
||||
(entry) =>
|
||||
!filter ||
|
||||
entry.domain.toLowerCase().includes(filter) ||
|
||||
entry.name.toLowerCase().includes(filter)
|
||||
)
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language))
|
||||
.map((entry) => entry.domain);
|
||||
}
|
||||
);
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
@@ -129,7 +156,13 @@ export class HaFilterDomains extends LitElement {
|
||||
}
|
||||
|
||||
private _handleItemSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||
const domains = this._domains(this.hass.states, this._filter, this.value);
|
||||
const domains = this._domains(
|
||||
this._states,
|
||||
this._localize,
|
||||
this._i18n.locale.language,
|
||||
this._filter,
|
||||
this.value
|
||||
);
|
||||
|
||||
const visibleDomains = new Set(domains);
|
||||
const preserved = (this.value || []).filter((d) => !visibleDomains.has(d));
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import {
|
||||
apiContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
} from "../data/context";
|
||||
import type { RelatedResult } from "../data/search";
|
||||
import { findRelated } from "../data/search";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import { loadVirtualizer } from "../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
@@ -22,7 +29,20 @@ import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@customElement("ha-filter-entities")
|
||||
export class HaFilterEntities extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
@state()
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@state()
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@@ -62,7 +82,7 @@ export class HaFilterEntities extends LitElement {
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this.hass.localize("ui.panel.config.entities.caption")}
|
||||
${this._localize("ui.panel.config.entities.caption")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
@@ -82,9 +102,10 @@ export class HaFilterEntities extends LitElement {
|
||||
<ha-list class="ha-scrollbar" multi>
|
||||
<lit-virtualizer
|
||||
.items=${this._entities(
|
||||
this.hass.states,
|
||||
this._states,
|
||||
this.type,
|
||||
this._filter || "",
|
||||
this._i18n.locale.language,
|
||||
this.value
|
||||
)}
|
||||
.keyFunction=${this._keyFunction}
|
||||
@@ -163,9 +184,10 @@ export class HaFilterEntities extends LitElement {
|
||||
|
||||
private _entities = memoizeOne(
|
||||
(
|
||||
states: HomeAssistant["states"],
|
||||
states: ContextType<typeof statesContext>,
|
||||
type: this["type"],
|
||||
filter: string,
|
||||
language: string | undefined,
|
||||
_value
|
||||
) => {
|
||||
const values = Object.values(states);
|
||||
@@ -180,11 +202,7 @@ export class HaFilterEntities extends LitElement {
|
||||
.includes(filter))
|
||||
)
|
||||
.sort((a, b) =>
|
||||
stringCompare(
|
||||
computeStateName(a),
|
||||
computeStateName(b),
|
||||
this.hass.locale.language
|
||||
)
|
||||
stringCompare(computeStateName(a), computeStateName(b), language)
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -203,7 +221,7 @@ export class HaFilterEntities extends LitElement {
|
||||
|
||||
for (const entityId of this.value) {
|
||||
if (this.type) {
|
||||
relatedPromises.push(findRelated(this.hass, "entity", entityId));
|
||||
relatedPromises.push(findRelated(this._api, "entity", entityId));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
@@ -5,14 +6,21 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import {
|
||||
apiContext,
|
||||
areasContext,
|
||||
floorsContext,
|
||||
internationalizationContext,
|
||||
} from "../data/context";
|
||||
import { getFloorAreaLookup } from "../data/floor_registry";
|
||||
import type { RelatedResult } from "../data/search";
|
||||
import { findRelated } from "../data/search";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-floor-icon";
|
||||
import "./ha-icon";
|
||||
@@ -26,7 +34,24 @@ import type { HaListSelectable } from "./list/ha-list-selectable";
|
||||
|
||||
@customElement("ha-filter-floor-areas")
|
||||
export class HaFilterFloorAreas extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: areasContext, subscribe: true })
|
||||
@state()
|
||||
private _areasReg!: ContextType<typeof areasContext>;
|
||||
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
@state()
|
||||
private _floorsReg!: ContextType<typeof floorsContext>;
|
||||
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@state()
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@property({ attribute: false }) public value?: {
|
||||
floors?: string[];
|
||||
@@ -55,7 +80,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const areas = this._areas(this.hass.areas, this.hass.floors);
|
||||
const areas = this._areas(this._areasReg, this._floorsReg);
|
||||
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
@@ -65,7 +90,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this.hass.localize("ui.panel.config.areas.caption")}
|
||||
${this._localize("ui.panel.config.areas.caption")}
|
||||
${this.value?.areas?.length || this.value?.floors?.length
|
||||
? html`<div class="badge">
|
||||
${(this.value?.areas?.length || 0) +
|
||||
@@ -85,9 +110,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
multi
|
||||
@ha-list-item-selected=${this._handleAdded}
|
||||
@ha-list-item-deselected=${this._handleRemoved}
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.panel.config.areas.caption"
|
||||
)}
|
||||
aria-label=${this._localize("ui.panel.config.areas.caption")}
|
||||
>
|
||||
${repeat(
|
||||
areas?.floors || [],
|
||||
@@ -141,8 +164,8 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
.type=${"areas"}
|
||||
class=${classMap({
|
||||
rtl: computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
this._i18n.language,
|
||||
this._i18n.translationMetadata.translations
|
||||
),
|
||||
floor: hasFloor,
|
||||
})}
|
||||
@@ -225,7 +248,10 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
}
|
||||
|
||||
private _areas = memoizeOne(
|
||||
(areaReg: HomeAssistant["areas"], floorReg: HomeAssistant["floors"]) => {
|
||||
(
|
||||
areaReg: ContextType<typeof areasContext>,
|
||||
floorReg: ContextType<typeof floorsContext>
|
||||
) => {
|
||||
const areas = Object.values(areaReg);
|
||||
const floors = Object.values(floorReg);
|
||||
const floorAreaLookup = getFloorAreaLookup(areas);
|
||||
@@ -261,7 +287,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
if (this.value.areas) {
|
||||
for (const areaId of this.value.areas) {
|
||||
if (this.type) {
|
||||
relatedPromises.push(findRelated(this.hass, "area", areaId));
|
||||
relatedPromises.push(findRelated(this._api, "area", areaId));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -269,7 +295,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
if (this.value.floors) {
|
||||
for (const floorId of this.value.floors) {
|
||||
if (this.type) {
|
||||
relatedPromises.push(findRelated(this.hass, "floor", floorId));
|
||||
relatedPromises.push(findRelated(this._api, "floor", floorId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiCog, mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
@@ -6,13 +6,14 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { labelsContext } from "../data/context";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { internationalizationContext, labelsContext } from "../data/context";
|
||||
import type { LabelRegistryEntry } from "../data/label/label_registry";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
@@ -25,14 +26,20 @@ import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@customElement("ha-filter-labels")
|
||||
export class HaFilterLabels extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@state()
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
@state()
|
||||
private _labels?: LabelRegistryEntry[];
|
||||
@@ -45,7 +52,12 @@ export class HaFilterLabels extends LitElement {
|
||||
|
||||
private _filteredLabels = memoizeOne(
|
||||
// `_value` used to recalculate the memoization when the selection changes
|
||||
(labels: LabelRegistryEntry[], filter: string | undefined, _value) =>
|
||||
(
|
||||
labels: LabelRegistryEntry[],
|
||||
filter: string | undefined,
|
||||
language: string | undefined,
|
||||
_value
|
||||
) =>
|
||||
labels
|
||||
.filter(
|
||||
(label) =>
|
||||
@@ -54,11 +66,7 @@ export class HaFilterLabels extends LitElement {
|
||||
label.label_id.toLowerCase().includes(filter)
|
||||
)
|
||||
.sort((a, b) =>
|
||||
stringCompare(
|
||||
a.name || a.label_id,
|
||||
b.name || b.label_id,
|
||||
this.hass.locale.language
|
||||
)
|
||||
stringCompare(a.name || a.label_id, b.name || b.label_id, language)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -71,7 +79,7 @@ export class HaFilterLabels extends LitElement {
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this.hass.localize("ui.panel.config.labels.caption")}
|
||||
${this._localize("ui.panel.config.labels.caption")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
@@ -96,6 +104,7 @@ export class HaFilterLabels extends LitElement {
|
||||
this._filteredLabels(
|
||||
this._labels || [],
|
||||
this._filter,
|
||||
this._i18n.locale.language,
|
||||
this.value
|
||||
),
|
||||
(label) => label.label_id,
|
||||
@@ -129,7 +138,7 @@ export class HaFilterLabels extends LitElement {
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.labels.manage_labels")}
|
||||
${this._localize("ui.panel.config.labels.manage_labels")}
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
`;
|
||||
@@ -169,6 +178,7 @@ export class HaFilterLabels extends LitElement {
|
||||
const filteredLabels = this._filteredLabels(
|
||||
this._labels || [],
|
||||
this._filter,
|
||||
this._i18n.locale.language,
|
||||
this.value
|
||||
);
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
@@ -22,8 +21,6 @@ import "../panels/config/voice-assistants/expose/expose-assistant-icon";
|
||||
|
||||
@customElement("ha-filter-voice-assistants")
|
||||
export class HaFilterVoiceAssistants extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
@@ -78,7 +75,6 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
<voice-assistant-brand-icon
|
||||
slot="graphic"
|
||||
.voiceAssistantId=${voiceAssistantId}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
</voice-assistant-brand-icon>
|
||||
${voiceAssistants[voiceAssistantId].name}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { apiContext, formattersContext } from "../data/context";
|
||||
import "./ha-button";
|
||||
import type { LawnMowerEntity, LawnMowerEntityState } from "../data/lawn_mower";
|
||||
import { LawnMowerEntityFeature } from "../data/lawn_mower";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
interface LawnMowerAction {
|
||||
action: string;
|
||||
@@ -39,13 +42,19 @@ const LAWN_MOWER_ACTIONS: Partial<
|
||||
|
||||
@customElement("ha-lawn_mower-action-button")
|
||||
class HaLawnMowerActionButton extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
private _formatters?: ContextType<typeof formattersContext>;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: LawnMowerEntity;
|
||||
|
||||
public render() {
|
||||
const state = this.stateObj.state;
|
||||
const action = LAWN_MOWER_ACTIONS[state];
|
||||
const action = LAWN_MOWER_ACTIONS[this.stateObj.state];
|
||||
|
||||
if (action && supportsFeature(this.stateObj, action.feature)) {
|
||||
return html`
|
||||
@@ -55,14 +64,14 @@ class HaLawnMowerActionButton extends LitElement {
|
||||
.service=${action.service}
|
||||
size="s"
|
||||
>
|
||||
${this.hass.localize(`ui.card.lawn_mower.actions.${action.action}`)}
|
||||
${this._localize(`ui.card.lawn_mower.actions.${action.action}`)}
|
||||
</ha-button>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-button appearance="plain" disabled>
|
||||
${this.hass.formatEntityState(this.stateObj)}
|
||||
${this._formatters?.formatEntityState(this.stateObj)}
|
||||
</ha-button>
|
||||
`;
|
||||
}
|
||||
@@ -71,7 +80,7 @@ class HaLawnMowerActionButton extends LitElement {
|
||||
ev.stopPropagation();
|
||||
const stateObj = this.stateObj;
|
||||
const service = ev.target.service;
|
||||
this.hass.callService("lawn_mower", service, {
|
||||
this._api.callService("lawn_mower", service, {
|
||||
entity_id: stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -243,6 +243,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
items = multiTermSortedSearch(
|
||||
items,
|
||||
searchString,
|
||||
DEFAULT_SEARCH_KEYS,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
|
||||
@@ -492,6 +492,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
let filteredItems = multiTermSortedSearch<PickerComboBoxItem>(
|
||||
this._allItems,
|
||||
searchString,
|
||||
this.searchKeys || DEFAULT_SEARCH_KEYS,
|
||||
(item) => item.id,
|
||||
index
|
||||
);
|
||||
|
||||
@@ -78,7 +78,6 @@ export class HaPictureUpload extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-file-upload
|
||||
.hass=${this.hass}
|
||||
.icon=${mdiImagePlus}
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.picture-upload.label")}
|
||||
|
||||
@@ -37,7 +37,6 @@ export class HaFileSelector extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-file-upload
|
||||
.hass=${this.hass}
|
||||
.accept=${this.selector.file?.accept}
|
||||
.icon=${mdiFile}
|
||||
.label=${this.label}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiClose, mdiConnection, mdiMemory, mdiPencil, mdiUsb } from "@mdi/js";
|
||||
import Fuse from "fuse.js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
@@ -406,12 +405,11 @@ export class HaSerialPortSelector extends LitElement {
|
||||
}
|
||||
let groupItems: SerialPickerItem[] = grouped[type];
|
||||
if (searchString) {
|
||||
const fuseIndex = Fuse.createIndex(DEFAULT_SEARCH_KEYS, groupItems);
|
||||
groupItems = multiTermSortedSearch(
|
||||
groupItems,
|
||||
searchString,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
DEFAULT_SEARCH_KEYS,
|
||||
(item) => item.id
|
||||
);
|
||||
}
|
||||
if (!groupItems.length) {
|
||||
|
||||
@@ -23,7 +23,6 @@ export class HaThemeSelector extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-theme-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
|
||||
@@ -1116,6 +1116,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
return multiTermSortedSearch(
|
||||
items,
|
||||
searchTerm,
|
||||
weightedKeys,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
@@ -1232,7 +1233,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${(item as EntityComboBoxItem).stateObj}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`
|
||||
: type === "device" && (item as DevicePickerItem).domain
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import { internationalizationContext, uiContext } from "../data/context";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
import "./ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
|
||||
@@ -23,7 +25,13 @@ export class HaThemePicker extends LitElement {
|
||||
@property({ attribute: "include-default", type: Boolean })
|
||||
public includeDefault = false;
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
private _ui?: ContextType<typeof uiContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n?: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@@ -56,8 +64,8 @@ export class HaThemePicker extends LitElement {
|
||||
|
||||
private _getItems = () =>
|
||||
this._getThemeOptions(
|
||||
this.hass?.themes.themes || {},
|
||||
this.hass?.locale.language || "en",
|
||||
this._ui?.themes.themes || {},
|
||||
this._i18n?.locale.language || "en",
|
||||
this.includeDefault
|
||||
);
|
||||
|
||||
@@ -70,10 +78,10 @@ export class HaThemePicker extends LitElement {
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.label=${this.label ??
|
||||
this.hass?.localize("ui.components.theme-picker.theme") ??
|
||||
this._i18n?.localize("ui.components.theme-picker.theme") ??
|
||||
"Theme"}
|
||||
.placeholder=${this.noThemeLabel ??
|
||||
this.hass?.localize("ui.components.theme-picker.no_theme")}
|
||||
this._i18n?.localize("ui.components.theme-picker.no_theme")}
|
||||
.helper=${this.helper}
|
||||
.value=${this.value}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
|
||||
@@ -73,7 +73,6 @@ export class HaThemeSettings extends LitElement {
|
||||
${this.showThemePicker
|
||||
? html`
|
||||
<ha-theme-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.labels?.theme}
|
||||
.noThemeLabel=${this.labels?.noTheme}
|
||||
.value=${themeSettings?.theme || undefined}
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
mdiAlertCircleOutline,
|
||||
mdiCheckCircleOutline,
|
||||
mdiChevronDown,
|
||||
mdiCircleOffOutline,
|
||||
mdiHelpCircleOutline,
|
||||
mdiProgressClock,
|
||||
mdiProgressWrench,
|
||||
@@ -84,6 +85,11 @@ class HaTracePicker extends LitElement {
|
||||
"ui.panel.config.automation.trace.picker.debugged"
|
||||
);
|
||||
item.icon_path = mdiProgressWrench;
|
||||
} else if (trace.not_triggered) {
|
||||
item.secondary = this.hass.localize(
|
||||
"ui.panel.config.automation.trace.picker.not_triggered"
|
||||
);
|
||||
item.icon_path = mdiCircleOffOutline;
|
||||
} else if (trace.script_execution === "finished") {
|
||||
item.secondary = this.hass.localize(
|
||||
"ui.panel.config.automation.trace.picker.finished",
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { apiContext } from "../data/context";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-button";
|
||||
|
||||
const STATES_INTERCEPTABLE: Record<
|
||||
@@ -46,7 +49,10 @@ const STATES_INTERCEPTABLE: Record<
|
||||
|
||||
@customElement("ha-vacuum-state")
|
||||
export class HaVacuumState extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: HassEntity;
|
||||
|
||||
@@ -68,19 +74,19 @@ export class HaVacuumState extends LitElement {
|
||||
}
|
||||
|
||||
private _computeInterceptable(
|
||||
state: string,
|
||||
stateString: string,
|
||||
supportedFeatures: number | undefined
|
||||
) {
|
||||
return state in STATES_INTERCEPTABLE && supportedFeatures !== 0;
|
||||
return stateString in STATES_INTERCEPTABLE && supportedFeatures !== 0;
|
||||
}
|
||||
|
||||
private _computeLabel(state: string, interceptable: boolean) {
|
||||
private _computeLabel(stateString: string, interceptable: boolean) {
|
||||
return interceptable
|
||||
? this.hass.localize(
|
||||
`ui.card.vacuum.actions.${STATES_INTERCEPTABLE[state].action}`
|
||||
? this._localize(
|
||||
`ui.card.vacuum.actions.${STATES_INTERCEPTABLE[stateString].action}`
|
||||
)
|
||||
: this.hass.localize(
|
||||
`component.vacuum.entity_component._.state.${state}`
|
||||
: this._localize(
|
||||
`component.vacuum.entity_component._.state.${stateString}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,7 +94,7 @@ export class HaVacuumState extends LitElement {
|
||||
ev.stopPropagation();
|
||||
const stateObj = this.stateObj;
|
||||
const service = STATES_INTERCEPTABLE[stateObj.state].service;
|
||||
await this.hass.callService("vacuum", service, {
|
||||
await this._api.callService("vacuum", service, {
|
||||
entity_id: stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,39 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { consume } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { transform } from "../common/decorators/transform";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import {
|
||||
configContext,
|
||||
formattersContext,
|
||||
internationalizationContext,
|
||||
} from "../data/context";
|
||||
import type { FrontendLocaleData } from "../data/translation";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type {
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantFormatters,
|
||||
HomeAssistantInternationalization,
|
||||
} from "../types";
|
||||
|
||||
@customElement("ha-water_heater-state")
|
||||
export class HaWaterHeaterState extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
private _formatters?: HomeAssistantFormatters;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
|
||||
transformer: ({ locale }) => locale,
|
||||
})
|
||||
private _locale?: FrontendLocaleData;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _hassConfig?: HomeAssistantConfig;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: HassEntity;
|
||||
|
||||
@@ -16,17 +41,16 @@ export class HaWaterHeaterState extends LitElement {
|
||||
return html`
|
||||
<div class="target">
|
||||
<span class="state-label label">
|
||||
${this.hass.formatEntityState(this.stateObj)}
|
||||
${this._formatters?.formatEntityState(this.stateObj)}
|
||||
</span>
|
||||
<span class="label"
|
||||
>${this._computeTarget(this.hass, this.stateObj)}</span
|
||||
>
|
||||
<span class="label">${this._computeTarget()}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeTarget(hass: HomeAssistant, stateObj: HassEntity) {
|
||||
if (!hass || !stateObj) return null;
|
||||
private _computeTarget() {
|
||||
if (!this._locale || !this._hassConfig || !this.stateObj) return null;
|
||||
const stateObj = this.stateObj;
|
||||
// We're using "!= null" on purpose so that we match both null and undefined.
|
||||
|
||||
if (
|
||||
@@ -35,17 +59,17 @@ export class HaWaterHeaterState extends LitElement {
|
||||
) {
|
||||
return `${formatNumber(
|
||||
stateObj.attributes.target_temp_low,
|
||||
this.hass.locale
|
||||
this._locale
|
||||
)} – ${formatNumber(
|
||||
stateObj.attributes.target_temp_high,
|
||||
this.hass.locale
|
||||
)} ${hass.config.unit_system.temperature}`;
|
||||
this._locale
|
||||
)} ${this._hassConfig.config.unit_system.temperature}`;
|
||||
}
|
||||
if (stateObj.attributes.temperature != null) {
|
||||
return `${formatNumber(
|
||||
stateObj.attributes.temperature,
|
||||
this.hass.locale
|
||||
)} ${hass.config.unit_system.temperature}`;
|
||||
this._locale
|
||||
)} ${this._hassConfig.config.unit_system.temperature}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
|
||||
@@ -101,7 +101,6 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
</span>
|
||||
<ha-media-manage-button
|
||||
slot="actionItems"
|
||||
.hass=${this.hass}
|
||||
.currentItem=${this._currentItem}
|
||||
@media-refresh=${this._refreshMedia}
|
||||
></ha-media-manage-button>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiFolderEdit } from "@mdi/js";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { configContext } from "../../data/context";
|
||||
import type { MediaPlayerItem } from "../../data/media-player";
|
||||
import {
|
||||
isLocalMediaSourceContentId,
|
||||
isImageUploadMediaSourceContentId,
|
||||
} from "../../data/media_source";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-button";
|
||||
import { showMediaManageDialog } from "./show-media-manage-dialog";
|
||||
@@ -20,7 +23,13 @@ declare global {
|
||||
|
||||
@customElement("ha-media-manage-button")
|
||||
class MediaManageButton extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) currentItem?: MediaPlayerItem;
|
||||
|
||||
@@ -31,7 +40,7 @@ class MediaManageButton extends LitElement {
|
||||
!this.currentItem ||
|
||||
!(
|
||||
isLocalMediaSourceContentId(this.currentItem.media_content_id || "") ||
|
||||
(this.hass!.user?.is_admin &&
|
||||
(this._config.user?.is_admin &&
|
||||
isImageUploadMediaSourceContentId(this.currentItem.media_content_id))
|
||||
)
|
||||
) {
|
||||
@@ -40,9 +49,7 @@ class MediaManageButton extends LitElement {
|
||||
return html`
|
||||
<ha-button appearance="filled" size="s" @click=${this._manage}>
|
||||
<ha-svg-icon .path=${mdiFolderEdit} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.manage"
|
||||
)}
|
||||
${this._localize("ui.components.media-browser.file_management.manage")}
|
||||
</ha-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import { customElement, property } from "lit/decorators";
|
||||
export class HaProgressRing extends ProgressRing {
|
||||
@property() public size?: "tiny" | "small" | "medium" | "large";
|
||||
|
||||
public updated(changedProps: PropertyValues<this>) {
|
||||
super.updated(changedProps);
|
||||
protected willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (changedProps.has("size")) {
|
||||
switch (this.size) {
|
||||
|
||||
@@ -17,9 +17,10 @@ import type {
|
||||
ChooseActionTraceStep,
|
||||
TraceExtended,
|
||||
} from "../../data/trace";
|
||||
import { getDataFromPath } from "../../data/trace";
|
||||
import { getDataFromPath, isTriggerPath } from "../../data/trace";
|
||||
import "../../panels/logbook/ha-logbook-renderer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-alert";
|
||||
import "../ha-code-editor";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-tab-group";
|
||||
@@ -69,7 +70,7 @@ export class HaTracePathDetails extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="padded-box trace-info">
|
||||
${this._renderSelectedTraceInfo()}
|
||||
${this._renderNotTriggeredNotice()} ${this._renderSelectedTraceInfo()}
|
||||
</div>
|
||||
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
@@ -95,6 +96,22 @@ export class HaTracePathDetails extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderNotTriggeredNotice() {
|
||||
if (
|
||||
!this.trace.not_triggered ||
|
||||
!this.selected?.path ||
|
||||
!isTriggerPath(this.selected.path) ||
|
||||
!(this.selected.path in this.trace.trace)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<ha-alert alert-type="info">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.automation.trace.path.not_triggered"
|
||||
)}
|
||||
</ha-alert>`;
|
||||
}
|
||||
|
||||
private _renderSelectedTraceInfo() {
|
||||
const paths = this.trace.trace;
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ export class HatGraphNode extends LitElement {
|
||||
@property({ attribute: "not-enabled", reflect: true, type: Boolean })
|
||||
notEnabled = false;
|
||||
|
||||
@property({ attribute: "not-triggered", reflect: true, type: Boolean })
|
||||
notTriggered = false;
|
||||
|
||||
@property({ attribute: "graph-start", reflect: true, type: Boolean })
|
||||
graphStart = false;
|
||||
|
||||
@@ -127,6 +130,9 @@ export class HatGraphNode extends LitElement {
|
||||
--stroke-clr: var(--hover-clr);
|
||||
--icon-clr: var(--default-icon-clr);
|
||||
}
|
||||
:host([not-triggered]) circle {
|
||||
stroke-dasharray: 4 3;
|
||||
}
|
||||
:host([not-enabled]) circle {
|
||||
--stroke-clr: var(--disabled-clr);
|
||||
}
|
||||
|
||||
@@ -90,21 +90,27 @@ export class HatScriptGraph extends LitElement {
|
||||
|
||||
private _renderTrigger(config: Trigger, i: number) {
|
||||
const path = `trigger/${i}`;
|
||||
const track = this.trace && path in this.trace.trace;
|
||||
const tracked = this.trace && path in this.trace.trace;
|
||||
// A not-triggered trace records the trigger that evaluated a change but
|
||||
// decided not to fire. It is still selectable (to view the reason), but
|
||||
// must not be shown as the path that ran.
|
||||
const notTriggered = !!(tracked && this.trace.not_triggered);
|
||||
const track = tracked && !notTriggered;
|
||||
this.renderedNodes[path] = { config, path, type: "trigger" };
|
||||
if (track) {
|
||||
if (tracked) {
|
||||
this.trackedNodes[path] = this.renderedNodes[path];
|
||||
}
|
||||
return html`
|
||||
<hat-graph-node
|
||||
graph-start
|
||||
?track=${track}
|
||||
?not-triggered=${notTriggered}
|
||||
@focus=${this._selectNode(config, path, "trigger")}
|
||||
?active=${this.selected === path}
|
||||
.iconPath=${mdiAsterisk}
|
||||
.notEnabled=${"enabled" in config && config.enabled === false}
|
||||
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
|
||||
tabindex=${track ? "0" : "-1"}
|
||||
tabindex=${tracked ? "0" : "-1"}
|
||||
></hat-graph-node>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiCircle,
|
||||
mdiCircleOffOutline,
|
||||
mdiCircleOutline,
|
||||
mdiProgressClock,
|
||||
mdiProgressWrench,
|
||||
@@ -323,6 +324,23 @@ class ActionRenderer {
|
||||
}
|
||||
|
||||
private _handleTrigger(index: number, triggerStep: TriggerTraceStep): number {
|
||||
if (this.trace.not_triggered) {
|
||||
this._renderEntry(
|
||||
triggerStep.path,
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.trace.messages.evaluated_not_triggered",
|
||||
{
|
||||
time: formatDateTimeWithSeconds(
|
||||
new Date(triggerStep.timestamp),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
}
|
||||
),
|
||||
mdiCircleOffOutline
|
||||
);
|
||||
return index + 1;
|
||||
}
|
||||
this._renderEntry(
|
||||
triggerStep.path,
|
||||
this.hass.localize(
|
||||
@@ -725,6 +743,16 @@ export class HaAutomationTracer extends LitElement {
|
||||
),
|
||||
icon: mdiProgressWrench,
|
||||
};
|
||||
} else if (this.trace.not_triggered) {
|
||||
entry = {
|
||||
description: this.hass.localize(
|
||||
"ui.panel.config.automation.trace.messages.not_triggered",
|
||||
{
|
||||
time: renderFinishedAt(),
|
||||
}
|
||||
),
|
||||
icon: mdiCircleOffOutline,
|
||||
};
|
||||
} else if (this.trace.script_execution === "finished") {
|
||||
entry = {
|
||||
description: this.hass.localize(
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { configContext, uiContext } from "../data/context";
|
||||
import { voiceAssistants } from "../data/expose";
|
||||
import { brandsUrl } from "../util/brands-url";
|
||||
|
||||
@customElement("voice-assistant-brand-icon")
|
||||
export class VoiceAssistantBrandicon extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
private _ui!: ContextType<typeof uiContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
|
||||
@property({ attribute: false }) public voiceAssistantId!: string;
|
||||
|
||||
@@ -21,9 +28,9 @@ export class VoiceAssistantBrandicon extends LitElement {
|
||||
{
|
||||
domain: voiceAssistants[this.voiceAssistantId].domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
darkOptimized: this._ui.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
this._config.auth.data.hassUrl
|
||||
)}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createContext } from "@lit/context";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantApi,
|
||||
@@ -196,4 +196,33 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the related context to an entity (or clear it when no entity), so nearby
|
||||
* pickers float relevant entities.
|
||||
* @param node - The node to fire the event on.
|
||||
* @param context - The context to set, or undefined to clear.
|
||||
*/
|
||||
export const fireRelatedContext = (
|
||||
node: HTMLElement,
|
||||
context: RelatedContextItem | undefined
|
||||
): void => {
|
||||
fireEvent(node, "hass-related-context", context);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the related context to an entity (or clear it when no entity), so nearby
|
||||
* pickers float relevant entities. Fired by editors.
|
||||
* @param node - The node to fire the event on.
|
||||
* @param entityId - The entity to set, or undefined to clear.
|
||||
*/
|
||||
export const fireEntityRelatedContext = (
|
||||
node: HTMLElement,
|
||||
entityId: string | undefined
|
||||
): void => {
|
||||
fireRelatedContext(
|
||||
node,
|
||||
entityId ? { itemType: "entity", itemId: entityId } : undefined
|
||||
);
|
||||
};
|
||||
|
||||
// #endregion related-context
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { getEntityAreaId } from "../../common/entity/context/get_entity_context";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import type { RelatedIdSets } from "../../common/search/related-context";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
|
||||
import type { FuseWeightedKey } from "../../resources/fuseMultiTerm";
|
||||
@@ -12,6 +15,7 @@ import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
||||
export interface EntityComboBoxItem extends PickerComboBoxItem {
|
||||
domain_name?: string;
|
||||
stateObj?: HassEntity;
|
||||
relatedRank?: number;
|
||||
}
|
||||
|
||||
export const entityComboBoxKeys: FuseWeightedKey[] = [
|
||||
@@ -186,3 +190,72 @@ export const getEntities = (
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
const RELATED_RANK_UNRELATED = 3;
|
||||
|
||||
const entityRelatedRank = (
|
||||
entityId: string | undefined,
|
||||
related: RelatedIdSets,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"]
|
||||
): number => {
|
||||
if (!entityId) {
|
||||
return RELATED_RANK_UNRELATED;
|
||||
}
|
||||
if (related.entities.has(entityId)) {
|
||||
return 0;
|
||||
}
|
||||
const deviceId = entities[entityId]?.device_id;
|
||||
if (deviceId && related.devices.has(deviceId)) {
|
||||
return 1;
|
||||
}
|
||||
const areaId = getEntityAreaId(entityId, entities, devices);
|
||||
if (areaId && related.areas.has(areaId)) {
|
||||
return 2;
|
||||
}
|
||||
return RELATED_RANK_UNRELATED;
|
||||
};
|
||||
|
||||
/**
|
||||
* Annotate entity items with their closeness to the related context, so they
|
||||
* can be floated to the top. The entity itself ranks closest, then its device,
|
||||
* then its area; anything unrelated keeps the lowest rank.
|
||||
*/
|
||||
export const markEntitiesRelated = (
|
||||
items: EntityComboBoxItem[],
|
||||
related: RelatedIdSets,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"]
|
||||
): EntityComboBoxItem[] =>
|
||||
items.map((item) => ({
|
||||
...item,
|
||||
relatedRank: entityRelatedRank(
|
||||
item.stateObj?.entity_id,
|
||||
related,
|
||||
entities,
|
||||
devices
|
||||
),
|
||||
}));
|
||||
|
||||
/**
|
||||
* Sort entity items by related closeness (entity, then device, then area, then
|
||||
* the rest). Pass `language` to break ties within a tier alphabetically by
|
||||
* label; omit it to keep the incoming order (e.g. search relevance).
|
||||
*/
|
||||
export const sortEntitiesByRelatedRank = (
|
||||
items: EntityComboBoxItem[],
|
||||
language?: string
|
||||
): EntityComboBoxItem[] =>
|
||||
[...items].sort((a, b) => {
|
||||
const rankDiff =
|
||||
(a.relatedRank ?? RELATED_RANK_UNRELATED) -
|
||||
(b.relatedRank ?? RELATED_RANK_UNRELATED);
|
||||
if (rankDiff !== 0 || language === undefined) {
|
||||
return rankDiff;
|
||||
}
|
||||
return caseInsensitiveStringCompare(
|
||||
a.sorting_label ?? "",
|
||||
b.sorting_label ?? "",
|
||||
language
|
||||
);
|
||||
});
|
||||
|
||||
+4
-1
@@ -184,8 +184,11 @@ export const createHistoricState = (
|
||||
// translate the bare description here.
|
||||
export const localizeTriggerSource = (
|
||||
localize: LocalizeFunc,
|
||||
source: string
|
||||
source: string | null
|
||||
) => {
|
||||
if (!source) {
|
||||
return "";
|
||||
}
|
||||
for (const key of Object.keys(triggerPhrases) as TriggerPhraseKey[]) {
|
||||
const phrase = triggerPhrases[key];
|
||||
if (source.startsWith(phrase)) {
|
||||
|
||||
+17
-4
@@ -18,12 +18,17 @@ interface BaseTraceStep {
|
||||
export interface TriggerTraceStep extends BaseTraceStep {
|
||||
changed_variables: {
|
||||
trigger: {
|
||||
alias?: string;
|
||||
description: string;
|
||||
alias?: string | null;
|
||||
// Absent on not-triggered traces, which have no trigger description.
|
||||
description?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
};
|
||||
// Present on not-triggered traces: a machine-readable reason code explaining
|
||||
// why the trigger evaluated a relevant change but decided not to fire, plus
|
||||
// optional diagnostic context.
|
||||
result?: { reason: string; data?: Record<string, unknown> };
|
||||
}
|
||||
|
||||
export interface ConditionTraceStep extends BaseTraceStep {
|
||||
@@ -61,6 +66,7 @@ export interface ChooseChoiceActionTraceStep extends BaseTraceStep {
|
||||
|
||||
export type ActionTraceStep =
|
||||
| BaseTraceStep
|
||||
| TriggerTraceStep
|
||||
| ConditionTraceStep
|
||||
| CallServiceActionTraceStep
|
||||
| ChooseActionTraceStep
|
||||
@@ -73,6 +79,9 @@ interface BaseTrace {
|
||||
last_step: string | null;
|
||||
run_id: string;
|
||||
state: "running" | "stopped" | "debugged";
|
||||
// True for traces recording that a trigger evaluated a relevant change but
|
||||
// did not fire. These are counted separately from actual runs.
|
||||
not_triggered?: boolean;
|
||||
timestamp: {
|
||||
start: string;
|
||||
finish: string | null;
|
||||
@@ -93,7 +102,10 @@ interface BaseTrace {
|
||||
| "error"
|
||||
// The exception is in the trace itself or in the last element of the trace
|
||||
// Script execution stopped by async_stop called on the script run because home assistant is shutting down, script mode is SCRIPT_MODE_RESTART etc:
|
||||
| "cancelled";
|
||||
| "cancelled"
|
||||
// No action was executed because a trigger evaluated a relevant change but
|
||||
// decided not to fire; the reason is in the trigger step of the trace
|
||||
| "not_triggered";
|
||||
}
|
||||
|
||||
interface BaseTraceExtended {
|
||||
@@ -103,7 +115,8 @@ interface BaseTraceExtended {
|
||||
|
||||
export interface AutomationTrace extends BaseTrace {
|
||||
domain: "automation";
|
||||
trigger: string;
|
||||
// `null` for not-triggered traces, which have no trigger description.
|
||||
trigger: string | null;
|
||||
}
|
||||
|
||||
export interface AutomationTraceExtended
|
||||
|
||||
@@ -322,6 +322,7 @@ export interface ZWaveJSDataCollectionStatus {
|
||||
export interface ZWaveJSRefreshNodeStatusMessage {
|
||||
event: string;
|
||||
stage?: string;
|
||||
progress?: number;
|
||||
}
|
||||
|
||||
export interface ZWaveJSRebuildRoutesStatusMessage {
|
||||
|
||||
@@ -39,11 +39,7 @@ class EntityPreviewRow extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
const stateObj = this.stateObj;
|
||||
return html`<state-badge
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
stateColor
|
||||
></state-badge>
|
||||
return html`<state-badge .stateObj=${stateObj} stateColor></state-badge>
|
||||
<div class="name" .title=${computeStateName(stateObj)}>
|
||||
${computeStateName(stateObj)}
|
||||
</div>
|
||||
@@ -201,12 +197,7 @@ class EntityPreviewRow extends LitElement {
|
||||
stateObj.state === "on" || stateObj.state === "off" || noValue;
|
||||
return html`
|
||||
${showToggle
|
||||
? html`
|
||||
<ha-entity-toggle
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></ha-entity-toggle>
|
||||
`
|
||||
? html` <ha-entity-toggle .stateObj=${stateObj}></ha-entity-toggle> `
|
||||
: this.hass.formatEntityState(stateObj)}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -310,7 +310,6 @@ export class QuickBar extends LitElement {
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${(item as EntityComboBoxItem).stateObj}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`
|
||||
: "domain" in item && item.domain
|
||||
@@ -461,7 +460,8 @@ export class QuickBar extends LitElement {
|
||||
navigateItems = this._filterGroup(
|
||||
"navigate",
|
||||
navigateItems,
|
||||
filter
|
||||
filter,
|
||||
navigateComboBoxKeys
|
||||
) as NavigationComboBoxItem[];
|
||||
}
|
||||
|
||||
@@ -482,7 +482,8 @@ export class QuickBar extends LitElement {
|
||||
commandItems = this._filterGroup(
|
||||
"command",
|
||||
commandItems,
|
||||
filter
|
||||
filter,
|
||||
commandComboBoxKeys
|
||||
) as ActionCommandComboBoxItem[];
|
||||
}
|
||||
|
||||
@@ -512,7 +513,8 @@ export class QuickBar extends LitElement {
|
||||
this._filterGroup(
|
||||
"entity",
|
||||
entityItems,
|
||||
filter
|
||||
filter,
|
||||
entityComboBoxKeys
|
||||
) as EntityComboBoxItem[]
|
||||
);
|
||||
} else {
|
||||
@@ -548,7 +550,7 @@ export class QuickBar extends LitElement {
|
||||
|
||||
if (filter) {
|
||||
deviceItems = sortRelatedFirst(
|
||||
this._filterGroup("device", deviceItems, filter)
|
||||
this._filterGroup("device", deviceItems, filter, deviceComboBoxKeys)
|
||||
);
|
||||
} else {
|
||||
deviceItems = this._sortRelatedByLabel(deviceItems);
|
||||
@@ -580,7 +582,7 @@ export class QuickBar extends LitElement {
|
||||
|
||||
if (filter) {
|
||||
areaItems = sortRelatedFirst(
|
||||
this._filterGroup("area", areaItems, filter)
|
||||
this._filterGroup("area", areaItems, filter, areaComboBoxKeys)
|
||||
);
|
||||
} else {
|
||||
areaItems = this._sortRelatedByLabel(areaItems);
|
||||
@@ -658,13 +660,15 @@ export class QuickBar extends LitElement {
|
||||
private _filterGroup(
|
||||
type: QuickBarSection,
|
||||
items: PickerComboBoxItem[],
|
||||
searchTerm: string
|
||||
searchTerm: string,
|
||||
weightedKeys: FuseWeightedKey[]
|
||||
) {
|
||||
const fuseIndex = this._fuseIndexes[type](items);
|
||||
|
||||
return multiTermSortedSearch(
|
||||
items,
|
||||
searchTerm,
|
||||
weightedKeys,
|
||||
(item: PickerComboBoxItem) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
|
||||
@@ -64,13 +64,9 @@ export class CloudStepIntro extends LitElement {
|
||||
<div class="logos">
|
||||
<voice-assistant-brand-icon
|
||||
.voiceAssistantId=${"cloud.google_assistant"}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
</voice-assistant-brand-icon>
|
||||
<voice-assistant-brand-icon
|
||||
.voiceAssistantId=${"cloud.alexa"}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
<voice-assistant-brand-icon .voiceAssistantId=${"cloud.alexa"}>
|
||||
</voice-assistant-brand-icon>
|
||||
</div>
|
||||
<h2>
|
||||
|
||||
@@ -375,7 +375,6 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${(item as EntityComboBoxItem).stateObj}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`
|
||||
: type === "device" && (item as DevicePickerItem).domain
|
||||
@@ -780,6 +779,7 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
return multiTermSortedSearch<PickerComboBoxItem>(
|
||||
items,
|
||||
searchTerm,
|
||||
searchKeys,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
|
||||
@@ -493,7 +493,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
@click=${this._showHelp}
|
||||
></ha-icon-button>
|
||||
<ha-filter-floor-areas
|
||||
.hass=${this.hass}
|
||||
.type=${"automation"}
|
||||
.value=${this._filters["ha-filter-floor-areas"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
@@ -503,7 +502,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-floor-areas>
|
||||
<ha-filter-devices
|
||||
.hass=${this.hass}
|
||||
.type=${"automation"}
|
||||
.value=${this._filters["ha-filter-devices"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
@@ -513,7 +511,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-devices>
|
||||
<ha-filter-entities
|
||||
.hass=${this.hass}
|
||||
.type=${"automation"}
|
||||
.value=${this._filters["ha-filter-entities"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
@@ -523,7 +520,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-entities>
|
||||
<ha-filter-labels
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-labels"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
slot="filter-pane"
|
||||
@@ -542,7 +538,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-categories>
|
||||
<ha-filter-voice-assistants
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-voice-assistants"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
slot="filter-pane"
|
||||
|
||||
@@ -320,6 +320,9 @@ export class HaAutomationTrace extends LitElement {
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this.hass.loadBackendTranslation("triggers");
|
||||
this.hass.loadBackendTranslation("conditions");
|
||||
|
||||
if (!this.automationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,6 @@ export class DialogUploadBackup
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
<ha-file-upload
|
||||
.hass=${this.hass}
|
||||
.uploading=${this._uploading}
|
||||
.icon=${mdiFolderUpload}
|
||||
.accept=${SUPPORTED_UPLOAD_FORMAT}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { mdiDotsVertical } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { isNavigationClick } from "../../../common/dom/is-navigation-click";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
@@ -91,7 +92,11 @@ class PanelDeveloperTools extends LitElement {
|
||||
panel=${tab.panel}
|
||||
.active=${page === tab.panel}
|
||||
>
|
||||
${this.hass.localize(tab.translationKey)}
|
||||
<a
|
||||
href="/config/developer-tools/${tab.panel}"
|
||||
@click=${this._handleTabAnchorClick}
|
||||
>${this.hass.localize(tab.translationKey)}</a
|
||||
>
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
)}
|
||||
@@ -105,6 +110,14 @@ class PanelDeveloperTools extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleTabAnchorClick(ev: MouseEvent) {
|
||||
ev.stopPropagation();
|
||||
const href = isNavigationClick(ev);
|
||||
if (href) {
|
||||
navigate(href);
|
||||
}
|
||||
}
|
||||
|
||||
private _handlePageSelected(ev: CustomEvent<{ name: string }>) {
|
||||
const newPage = ev.detail.name;
|
||||
if (!newPage) {
|
||||
@@ -142,6 +155,16 @@ class PanelDeveloperTools extends LitElement {
|
||||
--ha-tab-indicator-color: var(--app-header-text-color, white);
|
||||
--ha-tab-track-color: transparent;
|
||||
}
|
||||
ha-tab-group-tab::part(base) {
|
||||
padding: 0;
|
||||
}
|
||||
ha-tab-group-tab a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1em 1.5em;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -171,10 +171,7 @@ export class DeveloperYamlConfig extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
domain="homeassistant"
|
||||
service="reload_all"
|
||||
<ha-call-service-button domain="homeassistant" service="reload_all"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.yaml.section.reloading.all"
|
||||
)}
|
||||
@@ -182,7 +179,6 @@ export class DeveloperYamlConfig extends LitElement {
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
domain="homeassistant"
|
||||
service="reload_core_config"
|
||||
>${this.hass.localize(
|
||||
@@ -194,7 +190,6 @@ export class DeveloperYamlConfig extends LitElement {
|
||||
(reloadable) => html`
|
||||
<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
.domain=${reloadable.domain}
|
||||
service="reload"
|
||||
>${reloadable.name}
|
||||
|
||||
@@ -842,7 +842,6 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
<ha-filter-floor-areas
|
||||
.hass=${this.hass}
|
||||
type="device"
|
||||
.value=${this._filters["ha-filter-floor-areas"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
@@ -871,7 +870,6 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-states>
|
||||
<ha-filter-labels
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-labels"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
slot="filter-pane"
|
||||
|
||||
@@ -7,9 +7,8 @@ import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../../../components/ha-select";
|
||||
import "../../../../components/input/ha-input";
|
||||
import "./ha-energy-upstream-device-picker";
|
||||
import type { HaInput } from "../../../../components/input/ha-input";
|
||||
import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy";
|
||||
import { energyStatisticHelpUrl } from "../../../../data/energy";
|
||||
@@ -123,27 +122,6 @@ export class DialogEnergyDeviceSettingsWater
|
||||
|
||||
const pickableUnit = this._volume_units?.join(", ") || "";
|
||||
|
||||
const includedInDeviceOptions = !this._possibleParents.length
|
||||
? [
|
||||
{
|
||||
value: "-",
|
||||
disabled: true,
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption_water.dialog.no_upstream_devices"
|
||||
),
|
||||
},
|
||||
]
|
||||
: this._possibleParents.map((stat) => ({
|
||||
value: stat.stat_consumption,
|
||||
label:
|
||||
stat.name ||
|
||||
getStatisticLabel(
|
||||
this.hass,
|
||||
stat.stat_consumption,
|
||||
this._params?.statsMetadata?.[stat.stat_consumption]
|
||||
),
|
||||
}));
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
@@ -205,20 +183,23 @@ export class DialogEnergyDeviceSettingsWater
|
||||
>
|
||||
</ha-input>
|
||||
|
||||
<ha-select
|
||||
<ha-energy-upstream-device-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption_water.dialog.included_in_device"
|
||||
)}
|
||||
.value=${this._device?.included_in_stat || ""}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption_water.dialog.included_in_device_helper"
|
||||
)}
|
||||
.value=${this._device?.included_in_stat}
|
||||
.possibleParents=${this._possibleParents}
|
||||
.statsMetadata=${this._params.statsMetadata}
|
||||
.emptyLabel=${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption_water.dialog.no_upstream_devices"
|
||||
)}
|
||||
.disabled=${!this._device}
|
||||
@selected=${this._parentSelected}
|
||||
clearable
|
||||
.options=${includedInDeviceOptions}
|
||||
>
|
||||
</ha-select>
|
||||
@value-changed=${this._parentChanged}
|
||||
></ha-energy-upstream-device-picker>
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
@@ -293,7 +274,7 @@ export class DialogEnergyDeviceSettingsWater
|
||||
this._updateDirtyState(this._device);
|
||||
}
|
||||
|
||||
private _parentSelected(ev: HaSelectSelectEvent<string, true>) {
|
||||
private _parentChanged(ev: ValueChangedEvent<string>) {
|
||||
const newDevice = {
|
||||
...this._device!,
|
||||
included_in_stat: ev.detail.value,
|
||||
@@ -324,7 +305,7 @@ export class DialogEnergyDeviceSettingsWater
|
||||
width: 100%;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
ha-select {
|
||||
ha-energy-upstream-device-picker {
|
||||
display: block;
|
||||
margin-top: var(--ha-space-4);
|
||||
width: 100%;
|
||||
|
||||
@@ -7,12 +7,8 @@ import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-select";
|
||||
import type {
|
||||
HaSelectOption,
|
||||
HaSelectSelectEvent,
|
||||
} from "../../../../components/ha-select";
|
||||
import "../../../../components/input/ha-input";
|
||||
import "./ha-energy-upstream-device-picker";
|
||||
import type { HaInput } from "../../../../components/input/ha-input";
|
||||
import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy";
|
||||
import { energyStatisticHelpUrl } from "../../../../data/energy";
|
||||
@@ -123,28 +119,6 @@ export class DialogEnergyDeviceSettings
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const includedInDeviceOptions: HaSelectOption[] = this._possibleParents
|
||||
.length
|
||||
? this._possibleParents.map((stat) => ({
|
||||
value: stat.stat_consumption,
|
||||
label:
|
||||
stat.name ||
|
||||
getStatisticLabel(
|
||||
this.hass,
|
||||
stat.stat_consumption,
|
||||
this._params?.statsMetadata?.[stat.stat_consumption]
|
||||
),
|
||||
}))
|
||||
: [
|
||||
{
|
||||
value: "-",
|
||||
disabled: true,
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption.dialog.no_upstream_devices"
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
@@ -205,20 +179,23 @@ export class DialogEnergyDeviceSettings
|
||||
>
|
||||
</ha-input>
|
||||
|
||||
<ha-select
|
||||
<ha-energy-upstream-device-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption.dialog.included_in_device"
|
||||
)}
|
||||
.value=${this._device?.included_in_stat || ""}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption.dialog.included_in_device_helper"
|
||||
)}
|
||||
.value=${this._device?.included_in_stat}
|
||||
.possibleParents=${this._possibleParents}
|
||||
.statsMetadata=${this._params.statsMetadata}
|
||||
.emptyLabel=${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption.dialog.no_upstream_devices"
|
||||
)}
|
||||
.disabled=${!this._device}
|
||||
@selected=${this._parentSelected}
|
||||
clearable
|
||||
.options=${includedInDeviceOptions}
|
||||
>
|
||||
</ha-select>
|
||||
@value-changed=${this._parentChanged}
|
||||
></ha-energy-upstream-device-picker>
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
@@ -293,7 +270,7 @@ export class DialogEnergyDeviceSettings
|
||||
this._updateDirtyState(this._device);
|
||||
}
|
||||
|
||||
private _parentSelected(ev: HaSelectSelectEvent<string, true>) {
|
||||
private _parentChanged(ev: ValueChangedEvent<string>) {
|
||||
const newDevice = {
|
||||
...this._device!,
|
||||
included_in_stat: ev.detail.value,
|
||||
@@ -326,7 +303,7 @@ export class DialogEnergyDeviceSettings
|
||||
ha-statistic-picker {
|
||||
width: 100%;
|
||||
}
|
||||
ha-select {
|
||||
ha-energy-upstream-device-picker {
|
||||
display: block;
|
||||
margin-top: var(--ha-space-4);
|
||||
width: 100%;
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiChartLine, mdiShape } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeEntityNameList } from "../../../../common/entity/compute_entity_name_display";
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeRTL } from "../../../../common/util/compute_rtl";
|
||||
import "../../../../components/entity/state-badge";
|
||||
import "../../../../components/ha-combo-box-item";
|
||||
import "../../../../components/ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "../../../../components/ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "../../../../components/ha-picker-field";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy";
|
||||
import { domainToName } from "../../../../data/integration";
|
||||
import {
|
||||
getStatisticLabel,
|
||||
type StatisticsMetaData,
|
||||
} from "../../../../data/recorder";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
|
||||
interface UpstreamDeviceComboBoxItem extends PickerComboBoxItem {
|
||||
stateObj?: HassEntity;
|
||||
}
|
||||
|
||||
const SEARCH_KEYS = [
|
||||
{ name: "primary", weight: 10 },
|
||||
{ name: "search_labels.entityName", weight: 10 },
|
||||
{ name: "search_labels.friendlyName", weight: 9 },
|
||||
{ name: "search_labels.deviceName", weight: 8 },
|
||||
{ name: "search_labels.areaName", weight: 6 },
|
||||
{ name: "search_labels.domainName", weight: 4 },
|
||||
{ name: "id", weight: 2 },
|
||||
];
|
||||
|
||||
@customElement("ha-energy-upstream-device-picker")
|
||||
export class HaEnergyUpstreamDevicePicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ attribute: "empty-label" }) public emptyLabel?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public possibleParents: DeviceConsumptionEnergyPreference[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
public statsMetadata?: Record<string, StatisticsMetaData>;
|
||||
|
||||
private _computeItem = (statisticId: string): UpstreamDeviceComboBoxItem => {
|
||||
// Use the energy config's custom display name when the user has set one.
|
||||
const name = this.possibleParents.find(
|
||||
(parent) => parent.stat_consumption === statisticId
|
||||
)?.name;
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
|
||||
if (stateObj) {
|
||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||
stateObj,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
return {
|
||||
id: statisticId,
|
||||
primary: name || entityName || deviceName || statisticId,
|
||||
secondary,
|
||||
stateObj,
|
||||
search_labels: {
|
||||
entityName: entityName || null,
|
||||
deviceName: deviceName || null,
|
||||
areaName: areaName || null,
|
||||
friendlyName,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const label = getStatisticLabel(
|
||||
this.hass,
|
||||
statisticId,
|
||||
this.statsMetadata?.[statisticId]
|
||||
);
|
||||
const isExternal = statisticId.includes(":") && !statisticId.includes(".");
|
||||
|
||||
if (isExternal) {
|
||||
const domainName = domainToName(
|
||||
this.hass.localize,
|
||||
statisticId.split(":")[0]
|
||||
);
|
||||
return {
|
||||
id: statisticId,
|
||||
primary: name || label,
|
||||
secondary: domainName,
|
||||
icon_path: mdiChartLine,
|
||||
search_labels: { label, domainName },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: statisticId,
|
||||
primary: name || label,
|
||||
secondary: this.hass.localize("ui.components.statistic-picker.no_state"),
|
||||
icon_path: mdiShape,
|
||||
search_labels: { label },
|
||||
};
|
||||
};
|
||||
|
||||
private _items = memoizeOne(
|
||||
(
|
||||
_hass: HomeAssistant,
|
||||
_value: string | undefined,
|
||||
_possibleParents: DeviceConsumptionEnergyPreference[],
|
||||
_statsMetadata?: Record<string, StatisticsMetaData>
|
||||
): UpstreamDeviceComboBoxItem[] => {
|
||||
const items = this.possibleParents.map((parent) =>
|
||||
this._computeItem(parent.stat_consumption)
|
||||
);
|
||||
|
||||
// Make sure the current value is selectable even if it is no longer
|
||||
// part of the possible parents, so it doesn't render as unknown.
|
||||
if (this.value && !items.some((item) => item.id === this.value)) {
|
||||
items.unshift(this._computeItem(this.value));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _getItems = () =>
|
||||
this._items(
|
||||
this.hass,
|
||||
this.value,
|
||||
this.possibleParents,
|
||||
this.statsMetadata
|
||||
);
|
||||
|
||||
private _renderItem = (item: UpstreamDeviceComboBoxItem) => html`
|
||||
${item.stateObj
|
||||
? html`
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${item.stateObj}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`
|
||||
: item.icon_path
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
style="margin: 0 4px"
|
||||
slot="start"
|
||||
.path=${item.icon_path}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
`;
|
||||
|
||||
private _rowRenderer: RenderItemFunction<UpstreamDeviceComboBoxItem> = (
|
||||
item,
|
||||
index
|
||||
) => html`
|
||||
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
|
||||
${this._renderItem(item)}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
private _valueRenderer: PickerValueRenderer = (value) =>
|
||||
this._renderItem(
|
||||
this._getItems().find((item) => item.id === value) ??
|
||||
this._computeItem(value)
|
||||
);
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
use-top-label
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.value=${this.value}
|
||||
.disabled=${this.disabled}
|
||||
.emptyLabel=${this.emptyLabel}
|
||||
.getItems=${this._getItems}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.searchKeys=${SEARCH_KEYS}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
this.value = value;
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-energy-upstream-device-picker": HaEnergyUpstreamDevicePicker;
|
||||
}
|
||||
}
|
||||
@@ -985,7 +985,6 @@ export class HaConfigEntities extends LitElement {
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
<ha-filter-floor-areas
|
||||
.hass=${this.hass}
|
||||
type="entity"
|
||||
.value=${this._filters["ha-filter-floor-areas"]}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
@@ -995,7 +994,6 @@ export class HaConfigEntities extends LitElement {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-floor-areas>
|
||||
<ha-filter-devices
|
||||
.hass=${this.hass}
|
||||
.type=${"entity"}
|
||||
.value=${this._filters["ha-filter-devices"]}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
@@ -1005,7 +1003,6 @@ export class HaConfigEntities extends LitElement {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-devices>
|
||||
<ha-filter-domains
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-domains"]}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
slot="filter-pane"
|
||||
@@ -1035,7 +1032,6 @@ export class HaConfigEntities extends LitElement {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-states>
|
||||
<ha-filter-labels
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-labels"]}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
slot="filter-pane"
|
||||
@@ -1044,7 +1040,6 @@ export class HaConfigEntities extends LitElement {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-labels>
|
||||
<ha-filter-voice-assistants
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-voice-assistants"]}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
slot="filter-pane"
|
||||
|
||||
@@ -669,7 +669,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
class=${this.narrow ? "narrow" : ""}
|
||||
>
|
||||
<ha-filter-floor-areas
|
||||
.hass=${this.hass}
|
||||
.type=${"entity"}
|
||||
.value=${this._filters["ha-filter-floor-areas"]}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
@@ -679,7 +678,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-floor-areas>
|
||||
<ha-filter-devices
|
||||
.hass=${this.hass}
|
||||
.type=${"entity"}
|
||||
.value=${this._filters["ha-filter-devices"]}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
@@ -689,7 +687,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-devices>
|
||||
<ha-filter-labels
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-labels"]}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
slot="filter-pane"
|
||||
@@ -708,7 +705,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-categories>
|
||||
<ha-filter-voice-assistants
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-voice-assistants"]}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
slot="filter-pane"
|
||||
|
||||
@@ -109,7 +109,6 @@ export class ZHAClusterAttributes extends LitElement {
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
domain="zha"
|
||||
service="set_zigbee_cluster_attribute"
|
||||
.data=${this._setAttributeServiceData}
|
||||
|
||||
@@ -96,7 +96,6 @@ export class ZHAClusterCommands extends LitElement {
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
domain="zha"
|
||||
service="issue_zigbee_cluster_command"
|
||||
.data=${this._issueClusterCommandServiceData}
|
||||
|
||||
@@ -91,7 +91,6 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
|
||||
<state-badge
|
||||
@click=${this._openMoreInfo}
|
||||
.title=${entity.stateName!}
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.hass!.states[entity.entity_id]}
|
||||
slot="item-icon"
|
||||
></state-badge>
|
||||
|
||||
+12
@@ -100,6 +100,8 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
|
||||
@state() private _lowSecurityReason?: number;
|
||||
|
||||
@state() private _interviewProgress?: number;
|
||||
|
||||
@state() private _device?: ZWaveJSAddNodeDevice;
|
||||
|
||||
@state() private _deviceOptions?: ZWaveJSAddNodeSmartStartOptions;
|
||||
@@ -339,6 +341,10 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
) {
|
||||
return html`
|
||||
<zwave-js-add-node-loading
|
||||
.hass=${this.hass}
|
||||
.progress=${this._step === "interviewing"
|
||||
? this._interviewProgress
|
||||
: undefined}
|
||||
.description=${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.add_node.${this._step !== "rename_device" ? "getting_device_information" : "saving_device"}`
|
||||
)}
|
||||
@@ -379,6 +385,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
}
|
||||
|
||||
return html`<zwave-js-add-node-loading
|
||||
.hass=${this.hass}
|
||||
.delay=${1000}
|
||||
></zwave-js-add-node-loading>`;
|
||||
}
|
||||
@@ -703,9 +710,13 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
break;
|
||||
case "node added":
|
||||
this._step = "interviewing";
|
||||
this._interviewProgress = undefined;
|
||||
this._lowSecurity = message.node.low_security;
|
||||
this._lowSecurityReason = message.node.low_security_reason;
|
||||
break;
|
||||
case "interview progress":
|
||||
this._interviewProgress = message.progress;
|
||||
break;
|
||||
case "interview completed":
|
||||
this._unsubscribeAddZwaveNode();
|
||||
this._step = "configure_device";
|
||||
@@ -1081,6 +1092,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
this._dskPin = "";
|
||||
this._lowSecurity = false;
|
||||
this._lowSecurityReason = undefined;
|
||||
this._interviewProgress = undefined;
|
||||
this._inclusionStrategy = undefined;
|
||||
|
||||
if (this._addNodeTimeoutHandle) {
|
||||
|
||||
+16
-1
@@ -1,21 +1,36 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
|
||||
import { blankBeforePercent } from "../../../../../../common/translations/blank_before_percent";
|
||||
import "../../../../../../components/animation/ha-fade-in";
|
||||
import "../../../../../../components/ha-spinner";
|
||||
import "../../../../../../components/progress/ha-progress-ring";
|
||||
import { WakeLockMixin } from "../../../../../../mixins/wakelock-mixin";
|
||||
import type { HomeAssistant } from "../../../../../../types";
|
||||
|
||||
@customElement("zwave-js-add-node-loading")
|
||||
export class ZWaveJsAddNodeLoading extends WakeLockMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public description?: string;
|
||||
|
||||
@property({ type: Number }) public progress?: number;
|
||||
|
||||
@property({ type: Number }) public delay = 0;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<ha-fade-in .delay=${this.delay}>
|
||||
<div class="loading">
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
${this.progress !== undefined
|
||||
? html`
|
||||
<ha-progress-ring size="large" .value=${this.progress}>
|
||||
${Math.round(this.progress)}${blankBeforePercent(
|
||||
this.hass.locale
|
||||
)}%
|
||||
</ha-progress-ring>
|
||||
`
|
||||
: html`<ha-spinner size="large"></ha-spinner>`}
|
||||
</div>
|
||||
${this.description ? html`<p>${this.description}</p>` : nothing}
|
||||
</ha-fade-in>
|
||||
|
||||
+35
-3
@@ -4,10 +4,12 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { blankBeforePercent } from "../../../../../common/translations/blank_before_percent";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-dialog-footer";
|
||||
import "../../../../../components/ha-dialog";
|
||||
import "../../../../../components/progress/ha-progress-ring";
|
||||
import { reinterviewZwaveNode } from "../../../../../data/zwave_js";
|
||||
import { haStyleDialog } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
@@ -21,8 +23,12 @@ class DialogZWaveJSReinterviewNode extends LitElement {
|
||||
|
||||
@state() private _status?: string;
|
||||
|
||||
// Completed interview stages for rendering checklists
|
||||
// Can be removed once min schema is bumped to 50
|
||||
@state() private _stages?: string[];
|
||||
|
||||
@state() private _progress?: number;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
private _subscribed?: Promise<UnsubscribeFunc>;
|
||||
@@ -31,6 +37,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
|
||||
params: ZWaveJSReinterviewNodeDialogParams
|
||||
): Promise<void> {
|
||||
this._stages = undefined;
|
||||
this._progress = undefined;
|
||||
this.device_id = params.device_id;
|
||||
this._open = true;
|
||||
}
|
||||
@@ -67,7 +74,21 @@ class DialogZWaveJSReinterviewNode extends LitElement {
|
||||
${this._status === "started"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-spinner></ha-spinner>
|
||||
${this._progress !== undefined
|
||||
? html`
|
||||
<ha-progress-ring
|
||||
size="large"
|
||||
.value=${this._progress}
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.reinterview_node.in_progress"
|
||||
)}
|
||||
>
|
||||
${Math.round(this._progress)}${blankBeforePercent(
|
||||
this.hass.locale
|
||||
)}%
|
||||
</ha-progress-ring>
|
||||
`
|
||||
: html`<ha-spinner></ha-spinner>`}
|
||||
<div class="status">
|
||||
<p>
|
||||
<b>
|
||||
@@ -119,7 +140,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ``}
|
||||
${this._stages
|
||||
${this._progress === undefined && this._stages
|
||||
? html`
|
||||
<div class="stages">
|
||||
${this._stages.map(
|
||||
@@ -173,7 +194,16 @@ class DialogZWaveJSReinterviewNode extends LitElement {
|
||||
if (message.event === "interview started") {
|
||||
this._status = "started";
|
||||
}
|
||||
if (message.event === "interview stage completed") {
|
||||
if (message.event === "interview progress") {
|
||||
this._status = "started";
|
||||
this._progress = message.progress;
|
||||
}
|
||||
// If upstream supports granular progress reporting,
|
||||
// ignore the legacy per-stage events that drive the stage checklist.
|
||||
if (
|
||||
message.event === "interview stage completed" &&
|
||||
this._progress === undefined
|
||||
) {
|
||||
if (this._stages === undefined) {
|
||||
this._stages = [message.stage];
|
||||
} else {
|
||||
@@ -205,6 +235,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
|
||||
this.device_id = undefined;
|
||||
this._status = undefined;
|
||||
this._stages = undefined;
|
||||
this._progress = undefined;
|
||||
|
||||
this._unsubscribe();
|
||||
|
||||
@@ -246,6 +277,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
|
||||
}
|
||||
|
||||
.flex-container ha-spinner,
|
||||
.flex-container ha-progress-ring,
|
||||
.flex-container ha-svg-icon {
|
||||
margin-right: 20px;
|
||||
margin-inline-end: 20px;
|
||||
|
||||
-1
@@ -130,7 +130,6 @@ class DialogZWaveJSUpdateFirmwareNode extends DirtyStateProviderMixin<FirmwareFo
|
||||
}
|
||||
|
||||
const beginFirmwareUpdateHTML = html`<ha-file-upload
|
||||
.hass=${this.hass}
|
||||
.uploading=${this._uploading}
|
||||
.icon=${mdiFileUpload}
|
||||
.label=${this.hass.localize(
|
||||
|
||||
@@ -190,10 +190,7 @@ export class SystemLogCard extends LitElement {
|
||||
>`}
|
||||
|
||||
<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
domain="system_log"
|
||||
service="clear"
|
||||
<ha-call-service-button domain="system_log" service="clear"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.logs.clear"
|
||||
)}</ha-call-service-button
|
||||
|
||||
@@ -512,7 +512,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-filter-floor-areas
|
||||
.hass=${this.hass}
|
||||
.type=${"scene"}
|
||||
.value=${this._filters["ha-filter-floor-areas"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
@@ -522,7 +521,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-floor-areas>
|
||||
<ha-filter-devices
|
||||
.hass=${this.hass}
|
||||
.type=${"scene"}
|
||||
.value=${this._filters["ha-filter-devices"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
@@ -532,7 +530,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-devices>
|
||||
<ha-filter-entities
|
||||
.hass=${this.hass}
|
||||
.type=${"scene"}
|
||||
.value=${this._filters["ha-filter-entities"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
@@ -542,7 +539,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-entities>
|
||||
<ha-filter-labels
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-labels"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
slot="filter-pane"
|
||||
@@ -561,7 +557,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-categories>
|
||||
<ha-filter-voice-assistants
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-voice-assistants"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
slot="filter-pane"
|
||||
|
||||
@@ -454,7 +454,6 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
|
||||
${this._mode === "live"
|
||||
? html`
|
||||
<state-badge
|
||||
.hass=${this.hass}
|
||||
.stateObj=${entityStateObj}
|
||||
slot="graphic"
|
||||
></state-badge>
|
||||
@@ -545,7 +544,6 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
|
||||
>
|
||||
${this._mode === "live"
|
||||
? html` <state-badge
|
||||
.hass=${this.hass}
|
||||
.stateObj=${entityStateObj}
|
||||
slot="graphic"
|
||||
></state-badge>`
|
||||
|
||||
@@ -483,7 +483,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
@click=${this._showHelp}
|
||||
></ha-icon-button>
|
||||
<ha-filter-floor-areas
|
||||
.hass=${this.hass}
|
||||
.type=${"script"}
|
||||
.value=${this._filters["ha-filter-floor-areas"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
@@ -493,7 +492,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-floor-areas>
|
||||
<ha-filter-devices
|
||||
.hass=${this.hass}
|
||||
.type=${"script"}
|
||||
.value=${this._filters["ha-filter-devices"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
@@ -503,7 +501,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-devices>
|
||||
<ha-filter-entities
|
||||
.hass=${this.hass}
|
||||
.type=${"script"}
|
||||
.value=${this._filters["ha-filter-entities"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
@@ -513,7 +510,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-entities>
|
||||
<ha-filter-labels
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-labels"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
slot="filter-pane"
|
||||
@@ -532,7 +528,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-categories>
|
||||
<ha-filter-voice-assistants
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-voice-assistants"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
slot="filter-pane"
|
||||
|
||||
@@ -107,10 +107,7 @@ export class AssistPref extends LitElement {
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
<h1 class="card-header">
|
||||
<voice-assistant-brand-icon
|
||||
.voiceAssistantId=${"conversation"}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
<voice-assistant-brand-icon .voiceAssistantId=${"conversation"}>
|
||||
</voice-assistant-brand-icon
|
||||
>Assist
|
||||
</h1>
|
||||
|
||||
@@ -64,10 +64,7 @@ export class CloudAlexaPref extends LitElement {
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
<h1 class="card-header">
|
||||
<voice-assistant-brand-icon
|
||||
.voiceAssistantId=${"cloud.alexa"}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
<voice-assistant-brand-icon .voiceAssistantId=${"cloud.alexa"}>
|
||||
</voice-assistant-brand-icon
|
||||
>${this.hass.localize("ui.panel.config.cloud.account.alexa.title")}
|
||||
</h1>
|
||||
|
||||
@@ -49,13 +49,9 @@ export class CloudDiscover extends LitElement {
|
||||
<div class="logos">
|
||||
<voice-assistant-brand-icon
|
||||
.voiceAssistantId=${"cloud.google_assistant"}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
</voice-assistant-brand-icon>
|
||||
<voice-assistant-brand-icon
|
||||
.voiceAssistantId=${"cloud.alexa"}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
<voice-assistant-brand-icon .voiceAssistantId=${"cloud.alexa"}>
|
||||
</voice-assistant-brand-icon>
|
||||
</div>
|
||||
<h2>
|
||||
|
||||
@@ -72,7 +72,6 @@ export class CloudGooglePref extends LitElement {
|
||||
<h1 class="card-header">
|
||||
<voice-assistant-brand-icon
|
||||
.voiceAssistantId=${"cloud.google_assistant"}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
</voice-assistant-brand-icon
|
||||
>${this.hass.localize("ui.panel.config.cloud.account.google.title")}
|
||||
|
||||
@@ -217,7 +217,6 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
|
||||
<voice-assistant-brand-icon
|
||||
slot="start"
|
||||
.voiceAssistantId=${key}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
</voice-assistant-brand-icon>
|
||||
<span slot="headline">${voiceAssistants[key].name}</span>
|
||||
|
||||
@@ -32,7 +32,6 @@ export class VoiceAssistantExposeAssistantIcon extends LitElement {
|
||||
filter: this.manual ? "grayscale(100%)" : undefined,
|
||||
})}
|
||||
.voiceAssistantId=${this.assistant}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
</voice-assistant-brand-icon>
|
||||
${this.unsupported
|
||||
|
||||
@@ -33,11 +33,7 @@ export class HomeFavoriteEntityListItem extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-settings-row slim>
|
||||
<state-badge
|
||||
slot="prefix"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></state-badge>
|
||||
<state-badge slot="prefix" .stateObj=${stateObj}></state-badge>
|
||||
<span slot="heading">${primary}</span>
|
||||
${secondary
|
||||
? html`<span slot="description">${secondary}</span>`
|
||||
|
||||
@@ -548,7 +548,6 @@ class HaLogbookEntry extends LitElement {
|
||||
></ha-state-icon>`;
|
||||
}
|
||||
return html`<state-badge
|
||||
.hass=${this.hass}
|
||||
.overrideIcon=${glyph.icon}
|
||||
.overrideImage=${this._brandImage(glyph.domain)}
|
||||
.stateColor=${false}
|
||||
|
||||
@@ -98,7 +98,10 @@ function createYAxisLabelFormatter(
|
||||
fractionDigits: number
|
||||
) {
|
||||
return (value: number): string =>
|
||||
formatNumber(value, locale, { maximumFractionDigits: fractionDigits });
|
||||
formatNumber(value, locale, {
|
||||
minimumFractionDigits: value === 0 ? 0 : fractionDigits,
|
||||
maximumFractionDigits: fractionDigits,
|
||||
});
|
||||
}
|
||||
|
||||
export function getCommonOptions(
|
||||
|
||||
@@ -49,6 +49,33 @@ export const computeShowHeaderToggle = <
|
||||
return !!config.show_header_toggle;
|
||||
};
|
||||
|
||||
export const migrateEntitiesCardConfig = (
|
||||
config: EntitiesCardConfig
|
||||
): EntitiesCardConfig => {
|
||||
let changed = false;
|
||||
const newEntities = config.entities?.map((e) => {
|
||||
if (typeof e !== "object") {
|
||||
return e;
|
||||
}
|
||||
if (!("format" in e)) {
|
||||
return e;
|
||||
}
|
||||
changed = true;
|
||||
const { format, ...rest } = e;
|
||||
return {
|
||||
...rest,
|
||||
time_format: (rest as EntityConfig).time_format ?? format,
|
||||
};
|
||||
});
|
||||
if (!changed) {
|
||||
return config;
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
entities: newEntities as (LovelaceRowConfig | string)[],
|
||||
};
|
||||
};
|
||||
|
||||
@customElement("hui-entities-card")
|
||||
class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
@@ -153,11 +180,12 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
throw new Error("Entities must be specified");
|
||||
}
|
||||
|
||||
const entities = processConfigEntities(config.entities);
|
||||
const migratedConfig = migrateEntitiesCardConfig(config);
|
||||
const entities = processConfigEntities(migratedConfig.entities);
|
||||
|
||||
this._config = config;
|
||||
this._config = migratedConfig;
|
||||
this._configEntities = entities;
|
||||
this._showHeaderToggle = computeShowHeaderToggle(config, entities);
|
||||
this._showHeaderToggle = computeShowHeaderToggle(migratedConfig, entities);
|
||||
if (this._config.header) {
|
||||
this._headerElement = createHeaderFooterElement(
|
||||
this._config.header
|
||||
|
||||
@@ -32,6 +32,34 @@ import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import "../components/hui-warning-element";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import type { GlanceCardConfig, GlanceConfigEntity } from "./types";
|
||||
import { TIMESTAMP_STATE_DOMAINS } from "../../../common/const";
|
||||
|
||||
export const migrateGlanceCardConfig = (
|
||||
config: GlanceCardConfig
|
||||
): GlanceCardConfig => {
|
||||
let changed = false;
|
||||
const newEntities = config.entities?.map((e) => {
|
||||
if (typeof e !== "object") {
|
||||
return e;
|
||||
}
|
||||
if (!("format" in e)) {
|
||||
return e;
|
||||
}
|
||||
changed = true;
|
||||
const { format, ...rest } = e;
|
||||
return {
|
||||
...rest,
|
||||
time_format: rest.time_format ?? format,
|
||||
};
|
||||
});
|
||||
if (!changed) {
|
||||
return config;
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
entities: newEntities as (GlanceConfigEntity | string)[],
|
||||
};
|
||||
};
|
||||
|
||||
@customElement("hui-glance-card")
|
||||
export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
@@ -78,14 +106,15 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
public setConfig(config: GlanceCardConfig): void {
|
||||
const migratedConfig = migrateGlanceCardConfig(config);
|
||||
this._config = {
|
||||
show_name: true,
|
||||
show_state: true,
|
||||
show_icon: true,
|
||||
state_color: true,
|
||||
...config,
|
||||
...migratedConfig,
|
||||
};
|
||||
const entities = processConfigEntities(config.entities).map(
|
||||
const entities = processConfigEntities(migratedConfig.entities).map(
|
||||
(entityConf) => ({
|
||||
hold_action: { action: "more-info" } as MoreInfoActionConfig,
|
||||
...entityConf,
|
||||
@@ -107,7 +136,8 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
}
|
||||
|
||||
const columns = config.columns || Math.min(config.entities.length, 5);
|
||||
const columns =
|
||||
migratedConfig.columns || Math.min(migratedConfig.entities.length, 5);
|
||||
this.style.setProperty("--glance-column-width", `${100 / columns}%`);
|
||||
|
||||
this._configEntities = entities;
|
||||
@@ -256,6 +286,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
const name = this.hass!.formatEntityName(stateObj, entityConf.name);
|
||||
const domain = computeDomain(entityConf.entity);
|
||||
|
||||
return html`
|
||||
<div
|
||||
@@ -278,7 +309,6 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
${this._config!.show_icon
|
||||
? html`
|
||||
<state-badge
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.overrideIcon=${entityConf.icon}
|
||||
.overrideImage=${entityConf.image}
|
||||
@@ -290,17 +320,18 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
${this._config!.show_state && entityConf.show_state !== false
|
||||
? html`
|
||||
<div>
|
||||
${computeDomain(entityConf.entity) === "sensor" &&
|
||||
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(
|
||||
stateObj.attributes.device_class
|
||||
) &&
|
||||
${(TIMESTAMP_STATE_DOMAINS.has(domain) ||
|
||||
(domain === "sensor" &&
|
||||
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(
|
||||
stateObj.attributes.device_class
|
||||
))) &&
|
||||
stateObj.state !== UNAVAILABLE &&
|
||||
stateObj.state !== UNKNOWN
|
||||
? html`
|
||||
<hui-timestamp-display
|
||||
.hass=${this.hass}
|
||||
.ts=${new Date(stateObj.state)}
|
||||
.format=${entityConf.format ??
|
||||
.format=${entityConf.time_format ??
|
||||
(stateObj.attributes.device_class ===
|
||||
SENSOR_DEVICE_CLASS_UPTIME
|
||||
? "total"
|
||||
|
||||
@@ -334,7 +334,7 @@ export interface GlanceConfigEntity extends ConfigEntity {
|
||||
image?: string;
|
||||
show_state?: boolean;
|
||||
state_color?: boolean;
|
||||
format?: TimestampRenderingFormat;
|
||||
time_format?: TimestampRenderingFormat;
|
||||
}
|
||||
|
||||
export interface GlanceCardConfig extends LovelaceCardConfig {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { RelatedContextItem } from "../../../data/context";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { getConfigEntityId } from "./get-config-entity-id";
|
||||
|
||||
export const getConfigRelatedContext = (
|
||||
config: LovelaceCardConfig
|
||||
): RelatedContextItem | undefined => {
|
||||
if (config.type === "area" && typeof config.area === "string") {
|
||||
return { itemType: "area", itemId: config.area };
|
||||
}
|
||||
const entityId = getConfigEntityId(config);
|
||||
return entityId ? { itemType: "entity", itemId: entityId } : undefined;
|
||||
};
|
||||
@@ -72,7 +72,6 @@ export class HuiButtonsBase extends LitElement {
|
||||
? html`
|
||||
<state-badge
|
||||
title=${computeTooltip(this.hass, entityConf)}
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.overrideIcon=${entityConf.icon}
|
||||
.overrideImage=${entityConf.image}
|
||||
|
||||
@@ -82,7 +82,6 @@ export class HuiGenericEntityRow extends LitElement {
|
||||
)}
|
||||
>
|
||||
<state-badge
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.overrideIcon=${this.config.icon}
|
||||
.overrideImage=${this.config.image}
|
||||
|
||||
@@ -60,7 +60,7 @@ const LAZY_LOAD_TYPES = {
|
||||
attribute: () => import("../special-rows/hui-attribute-row"),
|
||||
text: () => import("../special-rows/hui-text-row"),
|
||||
};
|
||||
const DOMAIN_TO_ELEMENT_TYPE = {
|
||||
export const DOMAIN_TO_ELEMENT_TYPE = {
|
||||
_domain_not_found: "simple",
|
||||
alert: "toggle",
|
||||
automation: "toggle",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEntityRelatedContext } from "../../../../data/context";
|
||||
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
|
||||
import { stripDefaults } from "../../../../common/util/strip-defaults";
|
||||
import { withViewTransition } from "../../../../common/util/view-transition";
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
import "../../badges/hui-badge";
|
||||
import { getConfigEntityId } from "../../common/get-config-entity-id";
|
||||
import "../../sections/hui-section";
|
||||
import { addBadge, replaceBadge } from "../config-util";
|
||||
import { getBadgeDefaultConfig } from "../get-badge-default-config";
|
||||
@@ -139,25 +141,38 @@ export class HuiDialogEditBadge
|
||||
this._badgeConfig = undefined;
|
||||
this._error = undefined;
|
||||
this._documentationURL = undefined;
|
||||
this._updateRelatedContext(undefined);
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
if (
|
||||
!this._badgeConfig ||
|
||||
this._documentationURL !== undefined ||
|
||||
!changedProps.has("_badgeConfig")
|
||||
) {
|
||||
if (!changedProps.has("_badgeConfig")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldConfig = changedProps.get("_badgeConfig") as LovelaceBadgeConfig;
|
||||
if (this._badgeConfig && this._documentationURL === undefined) {
|
||||
const oldConfig = changedProps.get("_badgeConfig") as LovelaceBadgeConfig;
|
||||
|
||||
if (oldConfig?.type !== this._badgeConfig!.type) {
|
||||
this._documentationURL = this._badgeConfig!.type
|
||||
? getBadgeDocumentationURL(this.hass, this._badgeConfig!.type)
|
||||
: undefined;
|
||||
if (oldConfig?.type !== this._badgeConfig.type) {
|
||||
this._documentationURL = this._badgeConfig.type
|
||||
? getBadgeDocumentationURL(this.hass, this._badgeConfig.type)
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
this._updateRelatedContext(
|
||||
this._badgeConfig ? getConfigEntityId(this._badgeConfig) : undefined
|
||||
);
|
||||
}
|
||||
|
||||
private _relatedEntityId?: string;
|
||||
|
||||
private _updateRelatedContext(entityId: string | undefined): void {
|
||||
if (entityId === this._relatedEntityId) {
|
||||
return;
|
||||
}
|
||||
this._relatedEntityId = entityId;
|
||||
fireEntityRelatedContext(this, entityId);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
|
||||
@@ -86,7 +86,7 @@ export type EntityFuseIndex = FuseIndex<SearchableEntity>;
|
||||
// Fuse.createIndex (50-100ms on large registries).
|
||||
export const buildSearchIndex = (
|
||||
entities: SearchableEntity[]
|
||||
): EntityFuseIndex => Fuse.createIndex(entityComboBoxKeys, entities);
|
||||
): EntityFuseIndex => Fuse.createIndex(FUSE_KEY_NAMES, entities);
|
||||
|
||||
export const OTHER_AREAS_ID = "__other_areas__";
|
||||
const SEP = "~";
|
||||
@@ -100,6 +100,8 @@ export const domainKey = (parent: string, domain: string) =>
|
||||
`${parent}${SEP}dom|${domain}`;
|
||||
export const childKeyPrefix = (key: string) => `${key}${SEP}`;
|
||||
|
||||
const FUSE_KEY_NAMES = entityComboBoxKeys.map((k) => k.name as string);
|
||||
|
||||
export interface BuildEntityTreeInput {
|
||||
states: HomeAssistant["states"];
|
||||
entities: HomeAssistant["entities"];
|
||||
@@ -370,6 +372,7 @@ export function searchEntities(
|
||||
return multiTermSortedSearch(
|
||||
entities,
|
||||
filter,
|
||||
entityComboBoxKeys,
|
||||
(item) => item.id,
|
||||
index
|
||||
).slice(0, limit);
|
||||
|
||||
@@ -6,6 +6,10 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import {
|
||||
fireRelatedContext,
|
||||
type RelatedContextItem,
|
||||
} from "../../../../data/context";
|
||||
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
|
||||
import { stripDefaults } from "../../../../common/util/strip-defaults";
|
||||
import { withViewTransition } from "../../../../common/util/view-transition";
|
||||
@@ -33,6 +37,7 @@ import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
import "../../cards/hui-card";
|
||||
import { getConfigRelatedContext } from "../../common/get-config-related-context";
|
||||
import "../../sections/hui-section";
|
||||
import { getCardDefaultConfig } from "../get-card-default-config";
|
||||
import { getCardDocumentationURL } from "../get-dashboard-documentation-url";
|
||||
@@ -125,6 +130,7 @@ export class HuiDialogEditCard
|
||||
this._cardConfig = undefined;
|
||||
this._error = undefined;
|
||||
this._documentationURL = undefined;
|
||||
this._updateRelatedContext(undefined);
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -141,6 +147,21 @@ export class HuiDialogEditCard
|
||||
this._cardConfig!.type
|
||||
);
|
||||
}
|
||||
|
||||
this._updateRelatedContext(getConfigRelatedContext(this._cardConfig));
|
||||
}
|
||||
|
||||
private _relatedContext?: RelatedContextItem;
|
||||
|
||||
private _updateRelatedContext(context: RelatedContextItem | undefined): void {
|
||||
if (
|
||||
context?.itemType === this._relatedContext?.itemType &&
|
||||
context?.itemId === this._relatedContext?.itemId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._relatedContext = context;
|
||||
fireRelatedContext(this, context);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
|
||||
@@ -134,7 +134,6 @@ export class HuiEntityPickerTable extends LitElement {
|
||||
template: (entity) => html`
|
||||
<state-badge
|
||||
@click=${this._handleEntityClicked}
|
||||
.hass=${this.hass!}
|
||||
.stateObj=${entity.stateObj}
|
||||
></state-badge>
|
||||
`,
|
||||
|
||||
@@ -291,11 +291,7 @@ export class HuiSuggestionEntityTree extends LitElement {
|
||||
@click=${this._pickEntity}
|
||||
>
|
||||
${stateObj
|
||||
? html`<state-badge
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></state-badge>`
|
||||
? html`<state-badge slot="start" .stateObj=${stateObj}></state-badge>`
|
||||
: nothing}
|
||||
<span slot="headline">${item.name}</span>
|
||||
${secondary
|
||||
@@ -514,10 +510,7 @@ export class HuiSuggestionEntityTree extends LitElement {
|
||||
<div slot="start" class="leading">
|
||||
<span class="chevron-spacer"></span>
|
||||
${stateObj
|
||||
? html`<state-badge
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></state-badge>`
|
||||
? html`<state-badge .stateObj=${stateObj}></state-badge>`
|
||||
: nothing}
|
||||
</div>
|
||||
<span slot="headline">${name}</span>
|
||||
|
||||
@@ -168,11 +168,7 @@ export class HuiSuggestionPicker extends LitElement {
|
||||
</ha-section-title>
|
||||
<ha-combo-box-item compact class="selected-entity">
|
||||
${stateObj
|
||||
? html`<state-badge
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></state-badge>`
|
||||
? html`<state-badge slot="start" .stateObj=${stateObj}></state-badge>`
|
||||
: nothing}
|
||||
<span slot="headline">${primary}</span>
|
||||
${secondary
|
||||
|
||||
@@ -29,7 +29,10 @@ import "../../../../components/ha-theme-picker";
|
||||
import "../../../../components/input/ha-input";
|
||||
import { isCustomType } from "../../../../data/lovelace_custom_cards";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { computeShowHeaderToggle } from "../../cards/hui-entities-card";
|
||||
import {
|
||||
computeShowHeaderToggle,
|
||||
migrateEntitiesCardConfig,
|
||||
} from "../../cards/hui-entities-card";
|
||||
import type { EntitiesCardConfig } from "../../cards/types";
|
||||
import { processConfigEntities } from "../../common/process-config-entities";
|
||||
import { timeFormatConfigStruct } from "../../components/types";
|
||||
@@ -119,7 +122,7 @@ const attributeEntitiesRowConfigStruct = object({
|
||||
suffix: optional(string()),
|
||||
name: optional(string()),
|
||||
icon: optional(string()),
|
||||
format: optional(timeFormatConfigStruct),
|
||||
time_format: optional(timeFormatConfigStruct),
|
||||
});
|
||||
|
||||
const textEntitiesRowConfigStruct = object({
|
||||
@@ -207,9 +210,10 @@ export class HuiEntitiesCardEditor
|
||||
@state() private _subElementEditorConfig?: SubElementEditorConfig;
|
||||
|
||||
public setConfig(config: EntitiesCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
this._config = config;
|
||||
this._configEntities = processEditorEntities(config.entities);
|
||||
const migratedConfig = migrateEntitiesCardConfig(config);
|
||||
assert(migratedConfig, cardConfigStruct);
|
||||
this._config = migratedConfig;
|
||||
this._configEntities = processEditorEntities(migratedConfig.entities);
|
||||
}
|
||||
|
||||
private _showHeaderToggle = memoizeOne((config: EntitiesCardConfig) => {
|
||||
@@ -260,7 +264,6 @@ export class HuiEntitiesCardEditor
|
||||
@input=${this._valueChanged}
|
||||
></ha-input>
|
||||
<ha-theme-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._theme}
|
||||
.label=${`${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
|
||||
@@ -15,6 +15,8 @@ import { SENSOR_TIMESTAMP_DEVICE_CLASSES } from "../../../../data/sensor";
|
||||
import type { EntitiesCardEntityConfig } from "../../cards/types";
|
||||
import type { LovelaceRowEditor } from "../../types";
|
||||
import { entitiesConfigStruct } from "../structs/entities-struct";
|
||||
import { DOMAIN_TO_ELEMENT_TYPE } from "../../create-element/create-row-element";
|
||||
import { TIMESTAMP_STATE_DOMAINS } from "../../../../common/const";
|
||||
|
||||
const SECONDARY_INFO_VALUES = {
|
||||
none: {},
|
||||
@@ -88,7 +90,7 @@ export class HuiGenericEntityRowEditor
|
||||
...(showTimeFormat
|
||||
? ([
|
||||
{
|
||||
name: "format",
|
||||
name: "time_format",
|
||||
selector: {
|
||||
ui_time_format: {},
|
||||
},
|
||||
@@ -105,13 +107,17 @@ export class HuiGenericEntityRowEditor
|
||||
}
|
||||
|
||||
const entity = this._config.entity;
|
||||
const domain = entity ? computeDomain(entity) : undefined;
|
||||
const showTimeFormat =
|
||||
domain === "event" ||
|
||||
(domain === "sensor" &&
|
||||
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(
|
||||
this.hass.states[entity]?.attributes.device_class
|
||||
));
|
||||
const domain = entity ? computeDomain(entity) : "";
|
||||
const simpleEntity =
|
||||
(DOMAIN_TO_ELEMENT_TYPE[domain] ||
|
||||
DOMAIN_TO_ELEMENT_TYPE["_domain_not_found"]) === "simple";
|
||||
const showTimeFormat = simpleEntity
|
||||
? TIMESTAMP_STATE_DOMAINS.has(domain)
|
||||
: domain === "event" ||
|
||||
(domain === "sensor" &&
|
||||
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(
|
||||
this.hass.states[entity]?.attributes.device_class
|
||||
));
|
||||
|
||||
const schema =
|
||||
this.schema ||
|
||||
@@ -140,10 +146,6 @@ export class HuiGenericEntityRowEditor
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.entity-row.${schema.name}`
|
||||
);
|
||||
case "format":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.time_format`
|
||||
);
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
|
||||
@@ -11,11 +11,17 @@ import {
|
||||
string,
|
||||
union,
|
||||
} from "superstruct";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type {
|
||||
SchemaUnion,
|
||||
HaFormSchema,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { migrateGlanceCardConfig } from "../../cards/hui-glance-card";
|
||||
import type { ConfigEntity, GlanceCardConfig } from "../../cards/types";
|
||||
import "../../components/hui-entity-editor";
|
||||
import type { EntityConfig } from "../../entity-rows/types";
|
||||
@@ -26,6 +32,8 @@ import { processEditorEntities } from "../process-editor-entities";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { entitiesConfigStruct } from "../structs/entities-struct";
|
||||
import type { EditDetailElementEvent, SubElementEditorConfig } from "../types";
|
||||
import { TIMESTAMP_STATE_DOMAINS } from "../../../../common/const";
|
||||
import { SENSOR_TIMESTAMP_DEVICE_CLASSES } from "../../../../data/sensor";
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
@@ -41,59 +49,6 @@ const cardConfigStruct = assign(
|
||||
})
|
||||
);
|
||||
|
||||
const SUB_FORM = {
|
||||
schema: [
|
||||
{ name: "entity", selector: { entity: {} }, required: true },
|
||||
{
|
||||
name: "name",
|
||||
selector: { entity_name: {} },
|
||||
context: {
|
||||
entity: "entity",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
name: "",
|
||||
schema: [
|
||||
{
|
||||
name: "icon",
|
||||
selector: {
|
||||
icon: {},
|
||||
},
|
||||
context: {
|
||||
icon_entity: "entity",
|
||||
},
|
||||
},
|
||||
{ name: "show_last_changed", selector: { boolean: {} } },
|
||||
{ name: "show_state", selector: { boolean: {} }, default: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "tap_action",
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "more-info",
|
||||
},
|
||||
},
|
||||
context: ACTION_RELATED_CONTEXT,
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "optional_actions",
|
||||
flatten: true,
|
||||
schema: (["hold_action", "double_tap_action"] as const).map((action) => ({
|
||||
name: action,
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none" as const,
|
||||
},
|
||||
},
|
||||
context: ACTION_RELATED_CONTEXT,
|
||||
})),
|
||||
},
|
||||
] as const,
|
||||
};
|
||||
|
||||
const SCHEMA = [
|
||||
{ name: "title", selector: { text: {} } },
|
||||
{
|
||||
@@ -131,22 +86,97 @@ export class HuiGlanceCardEditor
|
||||
@state() private _configEntities?: ConfigEntity[];
|
||||
|
||||
public setConfig(config: GlanceCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
this._config = config;
|
||||
this._configEntities = processEditorEntities(config.entities);
|
||||
const migratedConfig = migrateGlanceCardConfig(config);
|
||||
assert(migratedConfig, cardConfigStruct);
|
||||
this._config = migratedConfig;
|
||||
this._configEntities = processEditorEntities(migratedConfig.entities);
|
||||
}
|
||||
|
||||
private _subForm = memoizeOne((showTimeFormat?: boolean) => ({
|
||||
schema: [
|
||||
{ name: "entity", selector: { entity: {} }, required: true },
|
||||
{
|
||||
name: "name",
|
||||
selector: { entity_name: {} },
|
||||
context: {
|
||||
entity: "entity",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
name: "",
|
||||
schema: [
|
||||
{
|
||||
name: "icon",
|
||||
selector: {
|
||||
icon: {},
|
||||
},
|
||||
context: {
|
||||
icon_entity: "entity",
|
||||
},
|
||||
},
|
||||
{ name: "show_last_changed", selector: { boolean: {} } },
|
||||
{ name: "show_state", selector: { boolean: {} }, default: true },
|
||||
],
|
||||
},
|
||||
...(showTimeFormat
|
||||
? ([
|
||||
{
|
||||
name: "time_format",
|
||||
selector: {
|
||||
ui_time_format: {},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
{
|
||||
name: "tap_action",
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "more-info",
|
||||
},
|
||||
},
|
||||
context: ACTION_RELATED_CONTEXT,
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "optional_actions",
|
||||
flatten: true,
|
||||
schema: (["hold_action", "double_tap_action"] as const).map(
|
||||
(action) => ({
|
||||
name: action,
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none" as const,
|
||||
},
|
||||
},
|
||||
context: ACTION_RELATED_CONTEXT,
|
||||
})
|
||||
),
|
||||
},
|
||||
] as const,
|
||||
}));
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (this._subElementEditorConfig) {
|
||||
const entity =
|
||||
this._configEntities![this._subElementEditorConfig.index!]?.entity;
|
||||
const domain = entity ? computeDomain(entity) : "";
|
||||
const showTimeFormat =
|
||||
TIMESTAMP_STATE_DOMAINS.has(domain) ||
|
||||
(domain === "sensor" &&
|
||||
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(
|
||||
this.hass.states[entity]?.attributes.device_class
|
||||
));
|
||||
return html`
|
||||
<hui-sub-element-editor
|
||||
.hass=${this.hass}
|
||||
.config=${this._subElementEditorConfig}
|
||||
.form=${SUB_FORM}
|
||||
.form=${this._subForm(showTimeFormat)}
|
||||
@go-back=${this._goBack}
|
||||
@config-changed=${this._handleSubEntityChanged}
|
||||
>
|
||||
|
||||
@@ -13,7 +13,7 @@ export const entitiesConfigStruct = union([
|
||||
icon: optional(string()),
|
||||
image: optional(string()),
|
||||
secondary_info: optional(string()),
|
||||
format: optional(timeFormatConfigStruct),
|
||||
time_format: optional(timeFormatConfigStruct),
|
||||
state_color: optional(boolean()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
hold_action: optional(actionConfigStruct),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user