Compare commits

...

19 Commits

Author SHA1 Message Date
Petar Petrov 9043262fab Dev tools -> Templates: observe tip height with ResizeObserver
Replaces the per-render scrollHeight read from #52012 with a
ResizeObserver started in firstUpdated, writing --tip-height directly to
the host style. Removes the need for a manual refresh when the viewport
crosses a wrap threshold, drops the unreachable isNaN check, and lets
the @query decorator stand in for the editor-tip id.
2026-05-14 15:40:18 +03:00
ildar170975 11afde6b5f Dev tools -> Templates: fix editor height (#52012)
* fix editor height

* get a height of ha-tip by `Element.scrollHeight`

* minor cleanup
2026-05-14 15:34:23 +03:00
pcan08 1b0dcb33b1 Add source filtering to media-player-source card feature (#52046)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Aidan Timson <aidan@timmo.dev>
2026-05-14 12:26:36 +00:00
Aidan Timson 67eecbc51d Remove "advanced" service controls (#52041)
Remove "Advanced" service controls
2026-05-14 15:19:52 +03:00
renovate[bot] 969ccf85d2 Update dependency @rsdoctor/rspack-plugin to v1.5.11 (#52040) 2026-05-14 08:51:19 +00:00
pcan08 500ce18ae5 Add volume mute button to media player playback card feature (#52029)
feat: add volume mute button to media player playback card feature

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 09:04:15 +03:00
pcan08 b413a7742c Add mute button to media player volume buttons card f… (#52028)
* feat(lovelace): add mute button to media player volume buttons card feature

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add show_mute_button config option to volume buttons feature

* feat: disable show_mute_button option when entity does not support mute

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Apply suggestions from code review

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-05-14 05:27:21 +00:00
karwosts e84373fdbd Fix energy device name dialog placeholders (#52032)
* Fix energy device placeholders

* array indexes
2026-05-14 08:17:55 +03:00
Simon Lamon caaee14856 Sync selected index in ha-list-base after initialization (#52033)
* Fix selection when they register after selectedIndices has been initialized

* Better fix
2026-05-14 08:16:35 +03:00
karwosts 28f04df81d Improve statistic picker handling of external stats (#52037) 2026-05-14 08:15:54 +03:00
Wendelin 48a8c5b2d5 Migrate ha-md-list to ha-list-base 1 (#52019)
* Migrate ha-md-list to ha-list-base

* Migrate ha-md-list to ha-list-base

* Next batch

* review
2026-05-13 19:51:31 +02:00
Petar Petrov 45312ba7fd Fix water sankey untracked consumption with nested sub-trackers (#51998) 2026-05-13 18:26:41 +02:00
Jan-Philipp Benecke b5dad80e19 Migrate ha-drawer to Web Awesome drawer (#51990)
* Migrate ha-drawer to Web Awesome drawer

* Make CI happy

* Implement swipe gesture support for ha-drawer and fix RTL support

* Fix CI

* Fix CI

* Readd border

* Fix sidebar

* Layout fix

* Fix sluggish scroll on mobile

* Fix CI

* Add transition
2026-05-13 17:01:39 +02:00
Aidan Timson ae85263d91 Add context for hass.format*, replace hass with lazy context on yaml/code editor (#52021)
* Add context for `hass.format*`

* Remove hass and use lazy context for ha-code-editor

* Remove hass and use context where hass isnt needed extensively

* Finish context switch for code editor

* Remove hass from yaml-editor calls

* Cleanup unused

* Update src/util/documentation-url.ts

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>

* Fix

---------

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-05-13 16:57:06 +02:00
ildar170975 c5000bcdde hui-history-graph-card-editor: add more options (#51749)
* add more options for history-graph-card

* add more options for history-graph-card

* Apply suggestion from @MindFreeze

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-05-13 14:13:40 +00:00
karwosts 5e085c70b0 Make gas & water sources nameable (#52011)
* Make gas & water sources nameable

* Add placeholders
2026-05-13 17:11:20 +03:00
Wendelin 71fc44284c Add tags to installed apps (#51987)
* Add supervisor app state tags and update translations

* Update styles

* Review

* Review

* unknown icon, lighter running green
2026-05-13 16:02:33 +02:00
Petar Petrov b7e1e23eaa Position chart tooltip beside cursor instead of over data point (#51904) 2026-05-13 15:47:15 +02:00
Aidan Timson 2ee7c6fc2a Add context to statistics panel (#52003)
* Add context to statistics panel

* Lazy context

* Cleanup

* Types

* Use api context, use registries, update helpers to only need api

* Infer type

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* Remove —

* Format

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-05-13 15:38:56 +03:00
140 changed files with 2046 additions and 1275 deletions
+1 -1
View File
@@ -142,7 +142,7 @@
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.1.0",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.5.10",
"@rsdoctor/rspack-plugin": "1.5.11",
"@rspack/core": "2.0.2",
"@rspack/dev-server": "2.0.1",
"@types/babel__plugin-transform-runtime": "7.9.5",
@@ -0,0 +1,40 @@
import type { TooltipPositionCallback } from "echarts/types/dist/shared";
export const TOOLTIP_GAP_PX = 12;
export const TOOLTIP_TOP_OFFSET_PX = 10;
/**
* Pins the tooltip near the top of the chart and offsets it horizontally
* from the cursor so it never covers the data point being inspected.
* For axis-trigger time-series tooltips where the cursor's Y is uncorrelated
* with the displayed content.
*/
export const sideTooltipPosition: TooltipPositionCallback = (
point,
_params,
dom,
_rect,
size
) => {
const [cursorX] = point;
const [viewW, viewH] = size.viewSize;
const [tipW, tipH] = size.contentSize;
const rtl =
dom instanceof HTMLElement && getComputedStyle(dom).direction === "rtl";
const rightOfCursor = cursorX + TOOLTIP_GAP_PX;
const leftOfCursor = cursorX - TOOLTIP_GAP_PX - tipW;
let x = rtl ? leftOfCursor : rightOfCursor;
const overflowsRight = x + tipW > viewW;
const overflowsLeft = x < 0;
if (overflowsRight || overflowsLeft) {
x = rtl ? rightOfCursor : leftOfCursor;
}
x = Math.max(0, Math.min(x, viewW - tipW));
const y = Math.max(0, Math.min(TOOLTIP_TOP_OFFSET_PX, viewH - tipH));
return [x, y];
};
@@ -11,6 +11,7 @@ import { computeRTL } from "../../common/util/compute_rtl";
import type { LineChartEntity, LineChartState } from "../../data/history";
import type { HomeAssistant } from "../../types";
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
import { sideTooltipPosition } from "./chart-tooltip-position";
import type { ECOption } from "../../resources/echarts/echarts";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import {
@@ -413,8 +414,7 @@ export class StateHistoryChartLine extends LitElement {
tooltip: {
trigger: "axis",
renderMode: "html",
position: "bottom",
align: "center",
position: sideTooltipPosition,
confine: true,
formatter: this._renderTooltip,
},
@@ -14,6 +14,7 @@ import { computeRTL } from "../../common/util/compute_rtl";
import type { TimelineEntity } from "../../data/history";
import type { HomeAssistant } from "../../types";
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
import { sideTooltipPosition } from "./chart-tooltip-position";
import { computeTimelineColor } from "./timeline-color";
import type { ECOption } from "../../resources/echarts/echarts";
import echarts from "../../resources/echarts/echarts";
@@ -263,8 +264,7 @@ export class StateHistoryChartTimeline extends LitElement {
},
tooltip: {
renderMode: "html",
position: "bottom",
align: "center",
position: sideTooltipPosition,
confine: true,
formatter: this._renderTooltip,
},
+2 -2
View File
@@ -39,6 +39,7 @@ import type { HomeAssistant } from "../../types";
import { getPeriodicAxisLabelConfig } from "./axis-label";
import type { CustomLegendOption } from "./ha-chart-base";
import "./ha-chart-base";
import { sideTooltipPosition } from "./chart-tooltip-position";
import { fillDataGapsAndRoundCaps } from "./round-caps";
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
@@ -459,8 +460,7 @@ export class StatisticsChart extends LitElement {
tooltip: {
trigger: "axis",
renderMode: "html",
position: "bottom",
align: "center",
position: sideTooltipPosition,
confine: true,
formatter: this._renderTooltip,
},
+9 -2
View File
@@ -142,6 +142,7 @@ export class HaStatisticPicker extends LitElement {
private async _getStatisticIds() {
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
this._picker?.requestUpdate();
this._valueRenderer = this._makeValueRenderer();
}
private _getItems = () =>
@@ -317,7 +318,7 @@ export class HaStatisticPicker extends LitElement {
}
);
private _valueRenderer: PickerValueRenderer = (value) => {
private _renderValue(value: string) {
const statisticId = value;
const item = this._computeItem(statisticId);
@@ -341,7 +342,13 @@ export class HaStatisticPicker extends LitElement {
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
`;
};
}
private _makeValueRenderer(): PickerValueRenderer {
return (value) => this._renderValue(value);
}
private _valueRenderer: PickerValueRenderer = this._makeValueRenderer();
private _computeItem(statisticId: string): StatisticComboBoxItem {
const stateObj = this.hass.states[statisticId];
+10 -12
View File
@@ -5,10 +5,10 @@ import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import type { Analytics, AnalyticsPreferences } from "../data/analytics";
import { haStyle } from "../resources/styles";
import "./ha-md-list-item";
import "./ha-switch";
import "./ha-tooltip";
import type { HaSwitch } from "./ha-switch";
import "./ha-tooltip";
import "./item/ha-row-item";
const ADDITIONAL_PREFERENCES = ["usage", "statistics"] as const;
@@ -33,7 +33,7 @@ export class HaAnalytics extends LitElement {
const baseEnabled = !loading && this.analytics!.preferences.base;
return html`
<ha-md-list-item>
<ha-row-item>
<span slot="headline"
>${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.preferences.base.title`
@@ -52,10 +52,10 @@ export class HaAnalytics extends LitElement {
.disabled=${loading}
name="base"
></ha-switch>
</ha-md-list-item>
</ha-row-item>
${ADDITIONAL_PREFERENCES.map(
(preference) => html`
<ha-md-list-item>
<ha-row-item>
<span slot="headline"
>${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.preferences.${preference}.title`
@@ -81,10 +81,10 @@ export class HaAnalytics extends LitElement {
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
)}
</ha-tooltip>`}
</ha-md-list-item>
</ha-row-item>
`
)}
<ha-md-list-item>
<ha-row-item>
<span slot="headline"
>${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.preferences.diagnostics.title`
@@ -103,7 +103,7 @@ export class HaAnalytics extends LitElement {
.disabled=${loading}
name="diagnostics"
></ha-switch>
</ha-md-list-item>
</ha-row-item>
`;
}
@@ -139,10 +139,8 @@ export class HaAnalytics extends LitElement {
color: var(--error-color);
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-item-overflow: visible;
ha-row-item {
--ha-row-item-padding-inline: 0;
}
`,
];
+98 -67
View File
@@ -27,6 +27,7 @@ import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, ReactiveElement, render } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { ContextType } from "@lit/context";
import { consume } from "@lit/context";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
@@ -43,7 +44,14 @@ import type {
import type { HomeAssistant } from "../types";
import { showToast } from "../util/toast";
import { documentationUrl } from "../util/documentation-url";
import { labelsContext } from "../data/context";
import {
internationalizationContext,
registriesContext,
statesContext,
labelsContext,
configContext,
formattersContext,
} from "../data/context";
import type { LabelRegistryEntry } from "../data/label/label_registry";
import "./ha-code-editor-completion-items";
import type { CompletionItem } from "./ha-code-editor-completion-items";
@@ -78,8 +86,6 @@ export class HaCodeEditor extends ReactiveElement {
@property() public mode = "yaml";
public hass?: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@@ -123,9 +129,29 @@ export class HaCodeEditor extends ReactiveElement {
@state() private _canCopy = false;
@consume({ context: labelsContext, subscribe: true })
@state()
private _labels?: LabelRegistryEntry[];
@consume({ context: configContext, subscribe: true })
private _config?: ContextType<typeof configContext>;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: labelsContext, subscribe: true })
private _labels?: ContextType<typeof labelsContext>;
@state()
@consume({ context: registriesContext, subscribe: true })
private _registries?: ContextType<typeof registriesContext>;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters?: ContextType<typeof formattersContext>;
@state()
@consume({ context: statesContext, subscribe: true })
private _states?: ContextType<typeof statesContext>;
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
private _loadedCodeMirror?: typeof import("../resources/codemirror");
@@ -189,9 +215,9 @@ export class HaCodeEditor extends ReactiveElement {
const line = doc.lineAt(pos);
const message = `${
err.reason ||
this.hass?.localize("ui.components.yaml-editor.error") ||
this._i18n?.localize("ui.components.yaml-editor.error") ||
"YAML syntax error"
}${err.mark ? ` (${this.hass?.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
}${err.mark ? ` (${this._i18n?.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
diagnostics = [{ from: pos, to: line.to, severity: "error", message }];
}
this.codemirror.dispatch(
@@ -396,8 +422,8 @@ export class HaCodeEditor extends ReactiveElement {
this._loadedCodeMirror!.haJinjaHoverSource(
view,
pos,
this.hass ? documentationUrl(this.hass, "") : undefined,
this.hass ? this._hassArgHoverContext() : undefined
this._config ? documentationUrl(this._config, "") : undefined,
this._hassArgHoverContext()
),
{ hoverTime: 300 }
),
@@ -408,7 +434,7 @@ export class HaCodeEditor extends ReactiveElement {
const completionSources: CompletionSource[] = [
this._loadedCodeMirror.haJinjaCompletionSource,
];
if (this.autocompleteEntities && this.hass) {
if (this.autocompleteEntities) {
completionSources.push(this._entityCompletions.bind(this));
}
if (this.autocompleteIcons) {
@@ -447,12 +473,12 @@ export class HaCodeEditor extends ReactiveElement {
private _fullscreenLabel(): string {
if (this._isFullscreen) {
return (
this.hass?.localize("ui.components.yaml-editor.exit_fullscreen") ||
this._i18n?.localize("ui.components.yaml-editor.exit_fullscreen") ||
"Exit fullscreen"
);
}
return (
this.hass?.localize("ui.components.yaml-editor.enter_fullscreen") ||
this._i18n?.localize("ui.components.yaml-editor.enter_fullscreen") ||
"Enter fullscreen"
);
}
@@ -507,7 +533,7 @@ export class HaCodeEditor extends ReactiveElement {
{
id: "test",
label:
this.hass?.localize(
this._i18n?.localize(
`ui.components.yaml-editor.test_${this.testing ? "off" : "on"}`
) || "Test",
path: this.testing ? mdiBugOutline : mdiBug,
@@ -518,14 +544,14 @@ export class HaCodeEditor extends ReactiveElement {
{
id: "undo",
disabled: !this._canUndo,
label: this.hass?.localize("ui.common.undo") || "Undo",
label: this._i18n?.localize("ui.common.undo") || "Undo",
path: mdiUndo,
action: (e: Event) => this._handleUndoClick(e),
},
{
id: "redo",
disabled: !this._canRedo,
label: this.hass?.localize("ui.common.redo") || "Redo",
label: this._i18n?.localize("ui.common.redo") || "Redo",
path: mdiRedo,
action: (e: Event) => this._handleRedoClick(e),
},
@@ -533,7 +559,7 @@ export class HaCodeEditor extends ReactiveElement {
id: "copy",
disabled: !this._canCopy,
label:
this.hass?.localize("ui.components.yaml-editor.copy_to_clipboard") ||
this._i18n?.localize("ui.components.yaml-editor.copy_to_clipboard") ||
"Copy to Clipboard",
path: mdiContentCopy,
action: (e: Event) => this._handleClipboardClick(e),
@@ -541,7 +567,7 @@ export class HaCodeEditor extends ReactiveElement {
{
id: "find-replace",
label:
this.hass?.localize("ui.components.yaml-editor.find_and_replace") ||
this._i18n?.localize("ui.components.yaml-editor.find_and_replace") ||
"Find and replace",
path: mdiFindReplace,
action: (e: Event) => this._handleFindReplaceClick(e),
@@ -583,7 +609,7 @@ export class HaCodeEditor extends ReactiveElement {
await copyToClipboard(this.value);
showToast(this, {
message:
this.hass?.localize("ui.common.copied_clipboard") ||
this._i18n?.localize("ui.common.copied_clipboard") ||
"Copied to clipboard",
});
}
@@ -651,12 +677,11 @@ export class HaCodeEditor extends ReactiveElement {
};
/**
* Builds a HassArgHoverContext from the current hass object so that
* Builds a HassArgHoverContext from the context objects so that
* haJinjaHoverSource can resolve entity / device / area friendly names
* without importing the full HomeAssistant type into the resource file.
*/
private _hassArgHoverContext(): HassArgHoverContext {
const hass = this.hass!;
const labelMap: Record<
string,
{ name: string; description?: string | null }
@@ -668,27 +693,33 @@ export class HaCodeEditor extends ReactiveElement {
};
}
return {
states: hass.states as HassArgHoverContext["states"],
devices: hass.devices as HassArgHoverContext["devices"],
areas: hass.areas as HassArgHoverContext["areas"],
floors: hass.floors as HassArgHoverContext["floors"],
entities: hass.entities as HassArgHoverContext["entities"],
states: this._states as HassArgHoverContext["states"],
devices: this._registries?.devices as HassArgHoverContext["devices"],
areas: this._registries?.areas as HassArgHoverContext["areas"],
floors: this._registries?.floors as HassArgHoverContext["floors"],
entities: this._registries?.entities as HassArgHoverContext["entities"],
labels: labelMap,
formatEntityState: (entityId) =>
hass.formatEntityState(hass.states[entityId]),
this._formatters!.formatEntityState(this._states![entityId]),
formatEntityName: (entityId) => {
const stateObj = hass.states[entityId];
const stateObj = this._states?.[entityId];
return (
(stateObj?.attributes.friendly_name as string | undefined) ??
hass.entities[entityId]?.name ??
this._registries?.entities?.[entityId]?.name ??
undefined
);
},
formatAttributeName: (entityId, attribute) =>
hass.formatEntityAttributeName(hass.states[entityId], attribute),
this._formatters!.formatEntityAttributeName(
this._states![entityId],
attribute
),
formatAttributeValue: (entityId, attribute) =>
hass.formatEntityAttributeValue(hass.states[entityId], attribute),
localize: (key) => hass.localize(key as never),
this._formatters!.formatEntityAttributeValue(
this._states![entityId],
attribute
),
localize: (key) => this._i18n!.localize(key as never),
};
}
@@ -698,49 +729,51 @@ export class HaCodeEditor extends ReactiveElement {
? completion.apply
: completion.label;
const context = getEntityContext(
this.hass!.states[key],
this.hass!.entities,
this.hass!.devices,
this.hass!.areas,
this.hass!.floors
this._states![key],
this._registries!.entities,
this._registries!.devices,
this._registries!.areas,
this._registries!.floors
);
const completionInfo = document.createElement("div");
completionInfo.classList.add("completion-info");
const formattedState = this.hass!.formatEntityState(this.hass!.states[key]);
const formattedState = this._formatters!.formatEntityState(
this._states![key]
);
const completionItems: CompletionItem[] = [
{
label: this.hass!.localize(
label: this._i18n!.localize(
"ui.components.entity.entity-state-picker.state"
),
value: formattedState,
subValue:
// If the state exactly matches the formatted state, don't show the raw state
this.hass!.states[key].state === formattedState
this._states![key].state === formattedState
? undefined
: this.hass!.states[key].state,
: this._states![key].state,
},
];
if (context.device && context.device.name) {
completionItems.push({
label: this.hass!.localize("ui.components.device-picker.device"),
label: this._i18n!.localize("ui.components.device-picker.device"),
value: context.device.name,
});
}
if (context.area && context.area.name) {
completionItems.push({
label: this.hass!.localize("ui.components.area-picker.area"),
label: this._i18n!.localize("ui.components.area-picker.area"),
value: context.area.name,
});
}
if (context.floor && context.floor.name) {
completionItems.push({
label: this.hass!.localize("ui.components.floor-picker.floor"),
label: this._i18n!.localize("ui.components.floor-picker.floor"),
value: context.floor.name,
});
}
@@ -761,15 +794,15 @@ export class HaCodeEditor extends ReactiveElement {
entityId: string,
attribute: string
): CompletionInfo | null => {
if (!this.hass) return null;
const stateObj = this.hass.states[entityId];
if (!this._states || !this._formatters) return null;
const stateObj = this._states[entityId];
if (!stateObj) return null;
const translatedName = this.hass.formatEntityAttributeName(
const translatedName = this._formatters.formatEntityAttributeName(
stateObj,
attribute
);
const formattedValue = this.hass.formatEntityAttributeValue(
const formattedValue = this._formatters.formatEntityAttributeValue(
stateObj,
attribute
);
@@ -809,9 +842,9 @@ export class HaCodeEditor extends ReactiveElement {
completion: Completion
): CompletionInfo | Promise<CompletionInfo> | null => {
if (
this.hass &&
this._states &&
typeof completion.apply === "string" &&
completion.apply in this.hass.states
completion.apply in this._states
) {
return this._renderInfo(completion);
}
@@ -1020,7 +1053,7 @@ export class HaCodeEditor extends ReactiveElement {
private _statesDotNotationCompletions(
context: CompletionContext
): CompletionResult | null | undefined {
if (!this.hass) return undefined;
if (!this._states) return undefined;
const { state: editorState, pos } = context;
const tree = this._loadedCodeMirror!.syntaxTree(editorState);
@@ -1129,9 +1162,7 @@ export class HaCodeEditor extends ReactiveElement {
case 0: {
// states. → offer all unique domains
const domains = [
...new Set(
Object.keys(this.hass.states).map((id) => id.split(".")[0])
),
...new Set(Object.keys(this._states).map((id) => id.split(".")[0])),
].sort();
return {
from: completionFrom,
@@ -1142,7 +1173,7 @@ export class HaCodeEditor extends ReactiveElement {
case 1: {
// states.<domain>. → offer entity object_ids for that domain
const [domain] = segments;
const entities = Object.keys(this.hass.states)
const entities = Object.keys(this._states)
.filter((id) => id.startsWith(`${domain}.`))
.map((id) => id.split(".").slice(1).join("."));
if (!entities.length) return { from: completionFrom, options: [] };
@@ -1172,7 +1203,7 @@ export class HaCodeEditor extends ReactiveElement {
}
// Offer attribute names from the entity's state object
const entityId = `${domain}.${entity}`;
const entityState = this.hass.states[entityId];
const entityState = this._states[entityId];
if (!entityState) return { from: completionFrom, options: [] };
const attrNames = Object.keys(entityState.attributes).sort();
return {
@@ -1342,8 +1373,8 @@ export class HaCodeEditor extends ReactiveElement {
): CompletionResult {
const from = stringNode.from + 1;
const empty: CompletionResult = { from, options: [] };
if (!entityId || !this.hass) return empty;
const entityState = this.hass.states[entityId];
if (!entityId || !this._states) return empty;
const entityState = this._states[entityId];
if (!entityState) return empty;
const attrs = Object.keys(entityState.attributes).sort();
if (!attrs.length) return empty;
@@ -1363,7 +1394,7 @@ export class HaCodeEditor extends ReactiveElement {
from: number;
to: number;
}): CompletionResult | null {
const states = this._getStates(this.hass!.states);
const states = this._getStates(this._states!);
if (!states?.length) return null;
// from is stringNode.from + 1 to skip the opening quote character.
const from = stringNode.from + 1;
@@ -1397,8 +1428,8 @@ export class HaCodeEditor extends ReactiveElement {
from: number;
to: number;
}): CompletionResult | null {
if (!this.hass?.devices) return null;
const devices = this._getDevices(this.hass.devices);
if (!this._registries?.devices) return null;
const devices = this._getDevices(this._registries.devices);
if (!devices.length) return null;
return {
from: stringNode.from + 1,
@@ -1426,8 +1457,8 @@ export class HaCodeEditor extends ReactiveElement {
from: number;
to: number;
}): CompletionResult | null {
if (!this.hass?.areas) return null;
const areas = this._getAreas(this.hass.areas);
if (!this._registries?.areas) return null;
const areas = this._getAreas(this._registries.areas);
if (!areas.length) return null;
return {
from: stringNode.from + 1,
@@ -1455,8 +1486,8 @@ export class HaCodeEditor extends ReactiveElement {
from: number;
to: number;
}): CompletionResult | null {
if (!this.hass?.floors) return null;
const floors = this._getFloors(this.hass.floors);
if (!this._registries?.floors) return null;
const floors = this._getFloors(this._registries.floors);
if (!floors.length) return null;
return {
from: stringNode.from + 1,
@@ -1556,7 +1587,7 @@ export class HaCodeEditor extends ReactiveElement {
// If cursor is after the entity field, show all entities
if (context.pos >= afterField) {
const states = this._getStates(this.hass!.states);
const states = this._getStates(this._states!);
if (!states || !states.length) {
return null;
@@ -1611,7 +1642,7 @@ export class HaCodeEditor extends ReactiveElement {
const afterListMarker = currentLine.from + listItemMatch[0].length;
if (context.pos >= afterListMarker) {
const states = this._getStates(this.hass!.states);
const states = this._getStates(this._states!);
if (!states || !states.length) {
return null;
@@ -1671,7 +1702,7 @@ export class HaCodeEditor extends ReactiveElement {
return null;
}
const states = this._getStates(this.hass!.states);
const states = this._getStates(this._states!);
if (!states || !states.length) {
return null;
+239 -123
View File
@@ -1,34 +1,109 @@
import { DrawerBase } from "@material/mwc-drawer/mwc-drawer-base";
import { styles } from "@material/mwc-drawer/mwc-drawer.css";
import type { PropertyValues } from "lit";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
import "@home-assistant/webawesome/dist/components/drawer/drawer";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { HASSDomEvent } from "../common/dom/fire_event";
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
declare global {
interface HASSDomEvents {
"hass-drawer-closed": undefined;
"hass-layout-transition": { active: boolean; reason?: string };
}
interface HTMLElementEventMap {
"hass-drawer-closed": HASSDomEvent<HASSDomEvents["hass-drawer-closed"]>;
"hass-layout-transition": HASSDomEvent<
HASSDomEvents["hass-layout-transition"]
>;
}
}
const blockingElements = (document as any).$blockingElements;
@customElement("ha-drawer")
export class HaDrawer extends DrawerBase {
@property() public direction: "ltr" | "rtl" = "ltr";
export class HaDrawer extends LitElement {
private static readonly _SWIPE_AXIS_TOLERANCE = 32;
private _mc?: HammerManager;
@property({ reflect: true }) public direction: "ltr" | "rtl" = "ltr";
private _rtlStyle?: HTMLElement;
@property() public type = "";
@property({ type: Boolean, reflect: true }) public open = false;
@query("wa-drawer") private _modalDrawer?: HTMLElement;
@query(".sidebar-shell") private _sidebarShell?: HTMLElement;
private _sidebarTransitionActive = false;
private _transitionTarget?: HTMLElement;
private _gestureRecognizer = new SwipeGestureRecognizer({
velocitySwipeThreshold: 0.35,
});
private _touchStartY = 0;
private _touchDeltaY = 0;
private get _modal() {
return this.type === "modal";
}
protected render(): TemplateResult {
return this._modal
? html`
<slot name="appContent"></slot>
<wa-drawer
placement="start"
.open=${this.open}
light-dismiss
without-header
@touchstart=${this._handleTouchStart}
@wa-after-hide=${this._handleAfterHide}
>
<slot></slot>
</wa-drawer>
`
: html`
<div class="layout">
<div class="sidebar-shell">
<slot></slot>
</div>
<div class="app-content">
<slot name="appContent"></slot>
</div>
</div>
`;
}
protected updated(_: PropertyValues<this>) {
this._syncTransitionListeners();
if (!this.open) {
this._resetSwipeTracking();
}
}
protected firstUpdated() {
this._syncTransitionListeners();
}
public disconnectedCallback() {
super.disconnectedCallback();
this._removeTransitionListeners();
this._unregisterSwipeHandlers();
}
private _handleAfterHide(ev: Event) {
ev.stopPropagation();
this.open = false;
fireEvent(this, "hass-drawer-closed");
}
private _closeModalDrawer() {
this.open = false;
}
private _handleDrawerTransitionStart = (ev: TransitionEvent) => {
if (ev.propertyName !== "width" || this._sidebarTransitionActive) {
return;
@@ -51,150 +126,191 @@ export class HaDrawer extends DrawerBase {
});
};
protected createAdapter() {
return {
...super.createAdapter(),
trapFocus: () => {
blockingElements.push(this);
this.appContent.inert = true;
document.body.style.overflow = "hidden";
},
releaseFocus: () => {
blockingElements.remove(this);
this.appContent.inert = false;
document.body.style.overflow = "";
},
};
private _handleTouchStart = (ev: TouchEvent) => {
if (!this._modal || !this.open) {
return;
}
const drawer = this._modalDrawer;
const dialog = drawer?.shadowRoot?.querySelector(
"dialog"
) as HTMLDialogElement | null;
if (!dialog) {
return;
}
const path = ev.composedPath();
if (!path.includes(dialog)) {
return;
}
ev.stopPropagation();
this._startSwipeTracking(ev.touches[0].clientX, ev.touches[0].clientY);
};
private _startSwipeTracking(clientX: number, clientY: number) {
document.addEventListener("touchmove", this._handleTouchMove, {
passive: true,
});
document.addEventListener("touchend", this._handleTouchEnd);
document.addEventListener("touchcancel", this._handleTouchEnd);
this._touchStartY = clientY;
this._touchDeltaY = 0;
this._gestureRecognizer.start(clientX);
}
protected updated(changedProps: PropertyValues<this>) {
super.updated(changedProps);
if (changedProps.has("direction")) {
this.mdcRoot.dir = this.direction;
if (this.direction === "rtl") {
this._rtlStyle = document.createElement("style");
this._rtlStyle.innerHTML = `
.mdc-drawer--animate {
transform: translateX(100%);
}
.mdc-drawer--opening {
transform: translateX(0);
}
.mdc-drawer--closing {
transform: translateX(100%);
}
`;
private _handleTouchMove = (ev: TouchEvent) => {
const currentX = ev.touches[0].clientX;
const currentY = ev.touches[0].clientY;
this._touchDeltaY = Math.abs(currentY - this._touchStartY);
this._gestureRecognizer.move(currentX);
};
this.shadowRoot!.appendChild(this._rtlStyle);
} else if (this._rtlStyle) {
this.shadowRoot!.removeChild(this._rtlStyle);
private _handleTouchEnd = () => {
this._unregisterSwipeHandlers();
const result = this._gestureRecognizer.end();
const isHorizontalGesture =
Math.abs(result.delta) >
this._touchDeltaY + HaDrawer._SWIPE_AXIS_TOLERANCE;
if (!isHorizontalGesture) {
this._resetSwipeTracking();
return;
}
const drawerDialog = this._modalDrawer?.shadowRoot?.querySelector(
'[part="dialog"]'
) as HTMLElement | null;
const drawerWidth = drawerDialog?.offsetWidth || 0;
if (result.isSwipe) {
const closeByVelocity =
this.direction === "rtl"
? result.isDownwardSwipe
: !result.isDownwardSwipe;
if (closeByVelocity) {
this._closeModalDrawer();
}
return;
}
if (changedProps.has("open") && this.open && this.type === "modal") {
this._setupSwipe();
} else if (this._mc) {
this._mc.destroy();
this._mc = undefined;
const closeByDistance =
drawerWidth > 0 &&
(this.direction === "rtl"
? result.delta > 0 && Math.abs(result.delta) > drawerWidth * 0.5
: result.delta < 0 && Math.abs(result.delta) > drawerWidth * 0.5);
if (closeByDistance) {
this._closeModalDrawer();
}
};
private _unregisterSwipeHandlers() {
document.removeEventListener("touchmove", this._handleTouchMove);
document.removeEventListener("touchend", this._handleTouchEnd);
document.removeEventListener("touchcancel", this._handleTouchEnd);
}
protected firstUpdated() {
super.firstUpdated();
this.mdcRoot?.addEventListener(
private _resetSwipeTracking() {
this._unregisterSwipeHandlers();
this._gestureRecognizer.reset();
this._touchStartY = 0;
this._touchDeltaY = 0;
}
private _syncTransitionListeners() {
if (this._transitionTarget === this._sidebarShell) {
return;
}
this._removeTransitionListeners();
if (!this._sidebarShell) {
return;
}
this._transitionTarget = this._sidebarShell;
this._transitionTarget.addEventListener(
"transitionstart",
this._handleDrawerTransitionStart
);
this.mdcRoot?.addEventListener(
this._transitionTarget.addEventListener(
"transitionend",
this._handleDrawerTransitionEnd
);
this.mdcRoot?.addEventListener(
this._transitionTarget.addEventListener(
"transitioncancel",
this._handleDrawerTransitionEnd
);
}
public disconnectedCallback() {
super.disconnectedCallback();
this.mdcRoot?.removeEventListener(
private _removeTransitionListeners() {
if (!this._transitionTarget) {
return;
}
this._transitionTarget.removeEventListener(
"transitionstart",
this._handleDrawerTransitionStart
);
this.mdcRoot?.removeEventListener(
this._transitionTarget.removeEventListener(
"transitionend",
this._handleDrawerTransitionEnd
);
this.mdcRoot?.removeEventListener(
this._transitionTarget.removeEventListener(
"transitioncancel",
this._handleDrawerTransitionEnd
);
this._transitionTarget = undefined;
}
private async _setupSwipe() {
const hammer = await import("../resources/hammer");
this._mc = new hammer.Manager(document, {
touchAction: "pan-y",
});
this._mc.add(
new hammer.Swipe({
direction:
this.direction === "rtl"
? hammer.DIRECTION_RIGHT
: hammer.DIRECTION_LEFT,
})
);
this._mc.on("swipeleft swiperight", () => {
fireEvent(this, "hass-toggle-menu", { open: false });
});
}
static styles = css`
:host {
display: block;
height: 100%;
}
static override styles = [
styles,
css`
.mdc-drawer {
position: fixed;
top: 0;
border-color: var(--divider-color, rgba(0, 0, 0, 0.12));
inset-inline-start: 0 !important;
inset-inline-end: initial !important;
transition-property: transform, width;
transition-duration:
var(--mdc-drawer-transition-duration, 0.2s),
var(--ha-animation-duration-normal);
transition-timing-function:
var(
--mdc-drawer-transition-timing-function,
cubic-bezier(0.4, 0, 0.2, 1)
),
ease;
}
.mdc-drawer.mdc-drawer--modal.mdc-drawer--open {
z-index: 200;
}
.mdc-drawer-app-content {
overflow: unset;
flex: none;
padding-left: var(--mdc-drawer-width);
padding-inline-start: var(--mdc-drawer-width);
padding-inline-end: initial;
direction: var(--direction);
width: 100%;
box-sizing: border-box;
transition:
padding-left var(--ha-animation-duration-normal) ease,
padding-inline-start var(--ha-animation-duration-normal) ease;
}
@media (prefers-reduced-motion: reduce) {
/* Use 1ms instead of "none" so the transitionend event still fires.
The MDC drawer foundation relies on it to complete the close cycle. */
.mdc-drawer,
.mdc-drawer-app-content {
transition: 1ms;
}
}
`,
];
.layout {
height: 100%;
}
.sidebar-shell {
position: fixed;
width: var(--ha-sidebar-width);
height: 100%;
border-inline-end: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12));
box-sizing: border-box;
transition: width var(--ha-animation-duration-normal) ease;
}
.app-content {
overflow: unset;
min-width: 0;
padding-inline-start: var(--ha-sidebar-width);
width: 100%;
height: 100%;
box-sizing: border-box;
transition:
padding-inline-start var(--ha-animation-duration-normal) ease,
width var(--ha-animation-duration-normal) ease;
}
wa-drawer {
--size: var(--ha-sidebar-width, 256px);
--show-duration: var(--ha-animation-duration-normal);
--hide-duration: var(--ha-animation-duration-normal);
}
wa-drawer::part(body) {
margin: 0;
padding: 0;
}
`;
}
declare global {
@@ -188,7 +188,6 @@ export class HaObjectSelector extends LitElement {
}
return html`<ha-yaml-editor
.hass=${this.hass}
.readonly=${this.disabled}
.label=${this.label}
.required=${this.required}
@@ -101,7 +101,6 @@ export class HaTemplateSelector extends LitElement {
: nothing}
<ha-code-editor
mode="jinja2"
.hass=${this.hass}
.value=${this.value}
.readOnly=${this.disabled}
.placeholder=${this.placeholder || "{{ ... }}"}
+1 -8
View File
@@ -86,9 +86,6 @@ export class HaServiceControl extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "show-advanced", type: Boolean })
public showAdvanced = false;
@property({ attribute: "show-service-id", type: Boolean })
public showServiceId = false;
@@ -545,7 +542,6 @@ export class HaServiceControl extends LitElement {
: ""}
${shouldRenderServiceDataYaml
? html`<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.components.service-control.action_data"
)}
@@ -667,10 +663,7 @@ export class HaServiceControl extends LitElement {
? this.hass.services[domain][serviceName].description_placeholders
: undefined;
return dataField.selector &&
(!dataField.advanced ||
this.showAdvanced ||
(this._value?.data && this._value.data[dataField.key] !== undefined))
return dataField.selector
? html`<ha-settings-row .narrow=${this.narrow}>
${!showOptional
? hasOptional
+10 -7
View File
@@ -3,14 +3,16 @@ import { DEFAULT_SCHEMA, dump, load } from "js-yaml";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import type { ContextType } from "@lit/context";
import { consume } from "@lit/context";
import { fireEvent } from "../common/dom/fire_event";
import { copyToClipboard } from "../common/util/copy-clipboard";
import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types";
import { showToast } from "../util/toast";
import "./ha-button";
import "./ha-code-editor";
import type { HaCodeEditor } from "./ha-code-editor";
import { internationalizationContext } from "../data/context";
const isEmpty = (obj: Record<string, unknown>): boolean => {
if (typeof obj !== "object" || obj === null) {
@@ -26,8 +28,6 @@ const isEmpty = (obj: Record<string, unknown>): boolean => {
@customElement("ha-yaml-editor")
export class HaYamlEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public value?: any;
@property({ attribute: false }) public yamlSchema: Schema = DEFAULT_SCHEMA;
@@ -59,6 +59,10 @@ export class HaYamlEditor extends LitElement {
@state() private _yaml = "";
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: ContextType<typeof internationalizationContext>;
@query("ha-code-editor") _codeEditor?: HaCodeEditor;
public setValue(value): void {
@@ -112,7 +116,6 @@ export class HaYamlEditor extends LitElement {
? html`<p>${this.label}${this.required ? " *" : ""}</p>`
: nothing}
<ha-code-editor
.hass=${this.hass}
.value=${this._yaml}
.readOnly=${this.readOnly}
.disableFullscreen=${this.disableFullscreen}
@@ -132,7 +135,7 @@ export class HaYamlEditor extends LitElement {
${this.copyClipboard
? html`
<ha-button appearance="plain" @click=${this._copyYaml}>
${this.hass.localize(
${this._i18n!.localize(
"ui.components.yaml-editor.copy_to_clipboard"
)}
</ha-button>
@@ -163,7 +166,7 @@ export class HaYamlEditor extends LitElement {
// Invalid YAML
isValid = false;
yamlError = err;
errorMsg = `${this.hass.localize("ui.components.yaml-editor.error", { reason: err.reason })}${err.mark ? ` (${this.hass.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
errorMsg = `${this._i18n!.localize("ui.components.yaml-editor.error", { reason: err.reason })}${err.mark ? ` (${this._i18n!.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
}
} else {
parsed = {};
@@ -201,7 +204,7 @@ export class HaYamlEditor extends LitElement {
if (this.yaml) {
await copyToClipboard(this.yaml);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
message: this._i18n!.localize("ui.common.copied_clipboard"),
});
}
}
+4 -8
View File
@@ -130,10 +130,6 @@ export class HaRowItem extends LitElement {
color: var(--primary-text-color);
font-size: var(--ha-font-size-m);
line-height: var(--ha-line-height-normal);
--ha-row-item-padding-block: var(--ha-space-3);
--ha-row-item-padding-inline: var(--ha-space-4);
--ha-row-item-gap: var(--ha-space-4);
--ha-row-item-min-height: 48px;
}
:host([disabled]) {
color: var(--disabled-text-color);
@@ -144,10 +140,10 @@ export class HaRowItem extends LitElement {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--ha-row-item-gap);
padding-block: var(--ha-row-item-padding-block);
padding-inline: var(--ha-row-item-padding-inline);
min-height: var(--ha-row-item-min-height);
gap: var(--ha-row-item-gap, var(--ha-space-4));
padding-block: var(--ha-row-item-padding-block, var(--ha-space-3));
padding-inline: var(--ha-row-item-padding-inline, var(--ha-space-4));
min-height: var(--ha-row-item-min-height, 48px);
box-sizing: border-box;
}
.content {
+2 -4
View File
@@ -292,14 +292,12 @@ export class HaListBase extends LitElement {
static styles: CSSResultGroup = css`
:host {
display: block;
--ha-list-gap: 0;
--ha-list-padding: 0;
}
.base {
display: flex;
flex-direction: column;
gap: var(--ha-list-gap);
padding: var(--ha-list-padding);
gap: var(--ha-list-gap, 0);
padding: var(--ha-list-padding, 0);
margin: 0;
list-style: none;
}
+3 -3
View File
@@ -121,15 +121,15 @@ export class HaListSelectable extends HaListBase {
public updateListItems() {
super.updateListItems();
this._syncItemSelectedState();
this._syncItemSelectedState(true);
}
private _sortedSelectedIndices(): number[] {
return [...this._selectedIndices!].sort((a, b) => a - b);
}
private _syncItemSelectedState() {
if (!this._selectedIndices) {
private _syncItemSelectedState(reset = false): void {
if (!this._selectedIndices || reset) {
this._selectedIndices = new Set<number>();
this.items.forEach((item, i) => {
const opt = item as HaListItemOption;
@@ -22,8 +22,6 @@ import "../../ha-adaptive-dialog";
import "../../ha-dialog-header";
import "../../ha-icon-button";
import "../../ha-icon-next";
import "../../ha-md-list";
import "../../ha-md-list-item";
import "../../ha-svg-icon";
import "../../list/ha-list-base";
import "../ha-target-picker-item-row";
@@ -28,8 +28,6 @@ import "../ha-domain-icon";
import { floorDefaultIconPath } from "../ha-floor-icon";
import "../ha-icon";
import "../ha-icon-button";
import "../ha-md-list";
import "../ha-md-list-item";
import "../ha-state-icon";
import "../ha-tooltip";
@@ -5,19 +5,15 @@ import { customElement, property } from "lit/decorators";
import "../ha-code-editor";
import "../ha-icon-button";
import type { TraceExtended } from "../../data/trace";
import type { HomeAssistant } from "../../types";
@customElement("ha-trace-blueprint-config")
export class HaTraceBlueprintConfig extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public trace!: TraceExtended;
protected render(): TemplateResult {
return html`
<ha-code-editor
.value=${dump(this.trace.blueprint_inputs || "").trimRight()}
.hass=${this.hass}
read-only
dir="ltr"
></ha-code-editor>
-4
View File
@@ -5,19 +5,15 @@ import { customElement, property } from "lit/decorators";
import "../ha-code-editor";
import "../ha-icon-button";
import type { TraceExtended } from "../../data/trace";
import type { HomeAssistant } from "../../types";
@customElement("ha-trace-config")
export class HaTraceConfig extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public trace!: TraceExtended;
protected render(): TemplateResult {
return html`
<ha-code-editor
.value=${dump(this.trace.config).trimRight()}
.hass=${this.hass}
read-only
dir="ltr"
></ha-code-editor>
@@ -271,7 +271,6 @@ export class HaTracePathDetails extends LitElement {
return config
? html`<ha-code-editor
.value=${dump(config).trimEnd()}
.hass=${this.hass}
read-only
dir="ltr"
></ha-code-editor>`
@@ -311,7 +310,6 @@ export class HaTracePathDetails extends LitElement {
: html`<ha-code-editor
read-only
dir="ltr"
.hass=${this.hass}
.value=${dump(trace.changed_variables).trimEnd()}
></ha-code-editor>`}
`
+9
View File
@@ -5,6 +5,7 @@ import type {
HomeAssistantApi,
HomeAssistantConfig,
HomeAssistantConnection,
HomeAssistantFormatters,
HomeAssistantInternationalization,
HomeAssistantRegistries,
HomeAssistantUI,
@@ -63,6 +64,14 @@ export const uiContext = createContext<HomeAssistantUI>("hassUi");
*/
export const configContext = createContext<HomeAssistantConfig>("hassConfig");
/**
* Entity formatting functions: `formatEntityState`, `formatEntityStateToParts`,
* `formatEntityAttributeValue`, `formatEntityAttributeValueToParts`,
* `formatEntityAttributeName`, and `formatEntityName`.
*/
export const formattersContext =
createContext<HomeAssistantFormatters>("hassFormatters");
/**
* Map of all entities in the entity registry, keyed by entity ID.
*/
+28
View File
@@ -3,6 +3,7 @@ import type {
HomeAssistantApi,
HomeAssistantConfig,
HomeAssistantConnection,
HomeAssistantFormatters,
HomeAssistantInternationalization,
HomeAssistantRegistries,
HomeAssistantUI,
@@ -156,6 +157,32 @@ const updateConfig = (
return value;
};
const updateFormatters = (
hass: HomeAssistant,
value?: HomeAssistantFormatters
): HomeAssistantFormatters => {
if (
!value ||
value.formatEntityState !== hass.formatEntityState ||
value.formatEntityStateToParts !== hass.formatEntityStateToParts ||
value.formatEntityAttributeValue !== hass.formatEntityAttributeValue ||
value.formatEntityAttributeValueToParts !==
hass.formatEntityAttributeValueToParts ||
value.formatEntityAttributeName !== hass.formatEntityAttributeName ||
value.formatEntityName !== hass.formatEntityName
) {
return {
formatEntityState: hass.formatEntityState,
formatEntityStateToParts: hass.formatEntityStateToParts,
formatEntityAttributeValue: hass.formatEntityAttributeValue,
formatEntityAttributeValueToParts: hass.formatEntityAttributeValueToParts,
formatEntityAttributeName: hass.formatEntityAttributeName,
formatEntityName: hass.formatEntityName,
};
}
return value;
};
export const updateHassGroups = {
registries: updateRegistries,
internationalization: updateInternationalization,
@@ -163,4 +190,5 @@ export const updateHassGroups = {
connection: updateConnection,
ui: updateUi,
config: updateConfig,
formatters: updateFormatters,
};
+4
View File
@@ -182,6 +182,8 @@ export interface GasSourceTypeEnergyPreference {
entity_energy_price: string | null;
number_energy_price: number | null;
unit_of_measurement?: string | null;
name?: string;
}
export interface WaterSourceTypeEnergyPreference {
@@ -200,6 +202,8 @@ export interface WaterSourceTypeEnergyPreference {
entity_energy_price: string | null;
number_energy_price: number | null;
unit_of_measurement?: string | null;
name?: string;
}
export type EnergySource =
+7 -4
View File
@@ -154,7 +154,7 @@ export const getRecorderInfo = (conn: Connection) =>
});
export const getStatisticIds = (
hass: HomeAssistant,
hass: Pick<HomeAssistant, "callWS">,
statistic_type?: "mean" | "sum"
) =>
hass.callWS<StatisticsMetaData[]>({
@@ -227,7 +227,7 @@ export const fetchStatistic = (
rolling_window: period.rolling_window,
});
export const validateStatistics = (hass: HomeAssistant) =>
export const validateStatistics = (hass: Pick<HomeAssistant, "callWS">) =>
hass.callWS<StatisticsValidationResults>({
type: "recorder/validate_statistics",
});
@@ -245,7 +245,10 @@ export const updateStatisticsMetadata = (
unit_class,
});
export const clearStatistics = (hass: HomeAssistant, statistic_ids: string[]) =>
export const clearStatistics = (
hass: Pick<HomeAssistant, "callWS">,
statistic_ids: string[]
) =>
hass.callWS<undefined>({
type: "recorder/clear_statistics",
statistic_ids,
@@ -369,5 +372,5 @@ export const getDisplayUnit = (
export const isExternalStatistic = (statisticsId: string): boolean =>
statisticsId.includes(":");
export const updateStatisticsIssues = (hass: HomeAssistant) =>
export const updateStatisticsIssues = (hass: Pick<HomeAssistant, "callWS">) =>
hass.callWS<undefined>({ type: "recorder/update_statistics_issues" });
@@ -2,11 +2,11 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-bottom-sheet";
import "../../components/ha-icon";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import "../../components/ha-svg-icon";
import "../../components/ha-dialog";
import "../../components/ha-icon";
import "../../components/ha-svg-icon";
import "../../components/item/ha-list-item-button";
import "../../components/list/ha-list-base";
import type { HomeAssistant } from "../../types";
import type { HassDialog } from "../make-dialog-manager";
import type { ListItemsDialogParams } from "./show-list-items-dialog";
@@ -51,41 +51,30 @@ export class ListItemsDialog
const content = html`
<div class="container">
<ha-md-list>
<ha-list-base>
${this._params.items.map(
(item) => html`
<ha-md-list-item
type="button"
@click=${this._itemClicked}
.item=${item}
>
<ha-list-item-button @click=${this._itemClicked} .item=${item}>
${item.iconPath
? html`
<ha-svg-icon
.path=${item.iconPath}
slot="start"
class="item-icon"
></ha-svg-icon>
`
: item.icon
? html`
<ha-icon
icon=${item.icon}
slot="start"
class="item-icon"
></ha-icon>
`
? html` <ha-icon icon=${item.icon} slot="start"></ha-icon> `
: nothing}
<span class="headline">${item.label}</span>
<span slot="headline">${item.label}</span>
${item.description
? html`
<span class="supporting-text">${item.description}</span>
<span slot="supporting-text">${item.description}</span>
`
: nothing}
</ha-md-list-item>
</ha-list-item-button>
`
)}
</ha-md-list>
</ha-list-base>
</div>
`;
@@ -113,12 +102,16 @@ export class ListItemsDialog
}
static styles = css`
ha-dialog {
ha-dialog,
ha-bottom-sheet {
/* Place above other dialogs */
--dialog-z-index: 104;
--dialog-content-padding: 0;
--md-list-item-leading-space: 24px;
--md-list-item-trailing-space: 24px;
--ha-row-item-padding-inline: var(--ha-space-6);
}
ha-bottom-sheet {
--ha-bottom-sheet-content-padding: var(--ha-space-4) 0 0;
}
`;
}
@@ -120,7 +120,6 @@ class MoreInfoScript extends LitElement {
...(this.data ? { data: this.data } : {}),
...this._scriptData,
}}
.showAdvanced=${this.hass.userData?.showAdvanced}
.narrow=${this.narrow}
@value-changed=${this._scriptDataChanged}
></ha-service-control>
@@ -9,10 +9,9 @@ import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-faded";
import "../../../components/ha-markdown";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-spinner";
import "../../../components/ha-switch";
import "../../../components/item/ha-row-item";
import "../../../components/progress/ha-progress-bar";
import type { BackupConfig } from "../../../data/backup";
import { fetchBackupConfig } from "../../../data/backup";
@@ -274,24 +273,22 @@ class MoreInfoUpdate extends LitElement {
<div class="footer">
${createBackupTexts
? html`
<ha-md-list>
<ha-md-list-item>
<span slot="headline">${createBackupTexts.title}</span>
${createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing}
<ha-switch
slot="end"
.checked=${this._createBackup}
@change=${this._createBackupChanged}
.disabled=${updateIsInstalling(this.stateObj)}
></ha-switch>
</ha-md-list-item>
</ha-md-list>
<ha-row-item>
<span slot="headline">${createBackupTexts.title}</span>
${createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing}
<ha-switch
slot="end"
.checked=${this._createBackup}
@change=${this._createBackupChanged}
.disabled=${updateIsInstalling(this.stateObj)}
></ha-switch>
</ha-row-item>
`
: nothing}
<div class="actions">
@@ -484,20 +481,9 @@ class MoreInfoUpdate extends LitElement {
z-index: 10;
}
ha-md-list {
ha-row-item {
width: 100%;
box-sizing: border-box;
margin-bottom: calc(var(--ha-space-4) * -1);
margin-top: calc(var(--ha-space-1) * -1);
--md-sys-color-surface: var(
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
}
ha-md-list-item {
--md-list-item-leading-space: var(--ha-space-6);
--md-list-item-trailing-space: var(--ha-space-6);
--ha-row-item-padding-inline: var(--ha-space-6);
}
.actions {
+8 -13
View File
@@ -2,8 +2,9 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../components/ha-alert";
import "../../components/ha-icon";
import "../../components/ha-md-list-item";
import "../../components/ha-spinner";
import "../../components/item/ha-list-item-button";
import "../../components/list/ha-list-base";
import type {
ExternalEntityAddToAction,
ExternalEntityAddToActions,
@@ -90,24 +91,23 @@ export class HaMoreInfoAddTo extends LitElement {
}
return html`
<div class="actions-list">
${this._externalActions.actions.map(
<ha-list-base>
${this._externalActions?.actions.map(
(action) => html`
<ha-md-list-item
type="button"
<ha-list-item-button
.disabled=${!action.enabled}
.action=${action}
@click=${this._actionSelected}
>
<ha-icon slot="start" .icon=${action.mdi_icon}></ha-icon>
<span>${action.name}</span>
<span slot="headline">${action.name}</span>
${action.details
? html`<span slot="supporting-text">${action.details}</span>`
: nothing}
</ha-md-list-item>
</ha-list-item-button>
`
)}
</div>
</ha-list-base>
`;
}
@@ -125,11 +125,6 @@ export class HaMoreInfoAddTo extends LitElement {
padding: var(--ha-space-8);
}
.actions-list {
display: flex;
flex-direction: column;
}
ha-icon {
display: flex;
align-items: center;
@@ -57,7 +57,6 @@ class HaMoreInfoDetails extends LitElement {
<div class="content">
${this.yamlMode
? html`<ha-yaml-editor
.hass=${this.hass}
.value=${yamlData}
read-only
auto-update
@@ -23,12 +23,16 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
@state() private _notifications: PersistentNotification[] = [];
@state() private _open = false;
@state() public _open = false;
@state() private _drawerOpen = false;
@query("ha-drawer") private _drawer?: HaDrawer;
private _unsubNotifications?: UnsubscribeFunc;
private _openAnimationFrame?: number;
connectedCallback() {
super.connectedCallback();
window.addEventListener("location-changed", this.closeDialog);
@@ -37,6 +41,10 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("location-changed", this.closeDialog);
if (this._openAnimationFrame !== undefined) {
cancelAnimationFrame(this._openAnimationFrame);
this._openAnimationFrame = undefined;
}
}
showDialog({ narrow }) {
@@ -51,22 +59,21 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
}
);
this.style.setProperty(
"--mdc-drawer-width",
"--ha-sidebar-width",
`min(100vw, calc(${narrow ? window.innerWidth + "px" : "500px"} + var(--safe-area-inset-left, 0px)))`
);
this._open = true;
}
closeDialog = () => {
if (this._drawer) {
if (this._drawerOpen && this._drawer) {
this._drawer.open = false;
this._drawerOpen = false;
return;
}
if (this._unsubNotifications) {
this._unsubNotifications();
this._unsubNotifications = undefined;
}
this._notifications = [];
fireEvent(this, "dialog-closed", { dialog: this.localName });
this._drawerOpen = false;
this._open = false;
this._finalizeClose();
};
public willUpdate(changedProps: PropertyValues<this>): void {
@@ -77,6 +84,17 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
}
}
protected updated(changedProps: PropertyValues<this>) {
super.updated(changedProps);
if (changedProps.has("_open") && this._open && !this._drawerOpen) {
this._openAnimationFrame = requestAnimationFrame(() => {
this._openAnimationFrame = undefined;
this._drawerOpen = true;
});
}
}
protected render() {
if (!this._open) {
return nothing;
@@ -104,8 +122,8 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
return html`
<ha-drawer
type="modal"
open
@MDCDrawer:closed=${this._dialogClosed}
.open=${this._drawerOpen}
@hass-drawer-closed=${this._dialogClosed}
.direction=${computeRTLDirection(this.hass)}
>
<ha-header-bar>
@@ -157,7 +175,9 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
private _dialogClosed(ev: Event) {
ev.stopPropagation();
this._drawerOpen = false;
this._open = false;
this._finalizeClose();
}
private _dismissAll() {
@@ -165,6 +185,19 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
this.closeDialog();
}
private _finalizeClose() {
if (this._openAnimationFrame !== undefined) {
cancelAnimationFrame(this._openAnimationFrame);
this._openAnimationFrame = undefined;
}
if (this._unsubNotifications) {
this._unsubNotifications();
this._unsubNotifications = undefined;
}
this._notifications = [];
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected supportedSingleKeyShortcuts(): SupportedShortcuts {
return {
Escape: () => this.closeDialog(),
+21 -28
View File
@@ -10,14 +10,15 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/animation/ha-fade-in";
import "../../components/ha-adaptive-dialog";
import "../../components/ha-alert";
import "../../components/ha-expansion-panel";
import "../../components/animation/ha-fade-in";
import "../../components/ha-icon-next";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import "../../components/ha-spinner";
import "../../components/item/ha-list-item-button";
import type { HaListItemButton } from "../../components/item/ha-list-item-button";
import "../../components/list/ha-list-base";
import "../../components/progress/ha-progress-bar";
import { fetchBackupInfo } from "../../data/backup";
import type { BackupManagerState } from "../../data/backup_manager";
@@ -130,9 +131,8 @@ class DialogRestart extends LitElement {
</div>
`
: html`
<ha-md-list dialogInitialFocus>
<ha-md-list-item
type="button"
<ha-list-base dialogInitialFocus>
<ha-list-item-button
@click=${this._reload}
.disabled=${this._loadingBackupInfo}
>
@@ -148,9 +148,8 @@ class DialogRestart extends LitElement {
<ha-svg-icon .path=${mdiAutoFix}></ha-svg-icon>
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
<ha-md-list-item
type="button"
</ha-list-item-button>
<ha-list-item-button
.action=${"restart"}
@click=${this._handleAction}
.disabled=${this._loadingBackupInfo}
@@ -167,18 +166,17 @@ class DialogRestart extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
</ha-md-list>
</ha-list-item-button>
</ha-list-base>
<ha-expansion-panel
.header=${this.hass.localize(
"ui.dialogs.restart.advanced_options"
)}
>
<ha-md-list>
<ha-list-base>
${showRebootShutdown
? html`
<ha-md-list-item
type="button"
<ha-list-item-button
.action=${"reboot"}
@click=${this._handleAction}
.disabled=${this._loadingBackupInfo}
@@ -197,9 +195,8 @@ class DialogRestart extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
<ha-md-list-item
type="button"
</ha-list-item-button>
<ha-list-item-button
.action=${"shutdown"}
@click=${this._handleAction}
.disabled=${this._loadingBackupInfo}
@@ -218,11 +215,10 @@ class DialogRestart extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
</ha-list-item-button>
`
: nothing}
<ha-md-list-item
type="button"
<ha-list-item-button
.action=${"restart-safe-mode"}
@click=${this._handleAction}
.disabled=${this._loadingBackupInfo}
@@ -244,8 +240,8 @@ class DialogRestart extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
</ha-md-list>
</ha-list-item-button>
</ha-list-base>
</ha-expansion-panel>
`}
</div>
@@ -324,16 +320,13 @@ class DialogRestart extends LitElement {
}
};
private async _handleAction(ev) {
private async _handleAction(ev: Event) {
if (this._loadingBackupInfo) {
return;
}
this._loadingBackupInfo = true;
const action = ev.currentTarget.action as
| "restart"
| "reboot"
| "shutdown"
| "restart-safe-mode";
const action = (ev.currentTarget as HaListItemButton & { action: string })
.action as "restart" | "reboot" | "shutdown" | "restart-safe-mode";
const backupState = await this._loadBackupState();
@@ -1,8 +1,9 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import "../../components/item/ha-list-item-button";
import type { HaListItemButton } from "../../components/item/ha-list-item-button";
import "../../components/list/ha-list-base";
import type { AssistSatelliteConfiguration } from "../../data/assist_satellite";
import { setWakeWords } from "../../data/assist_satellite";
import type { HomeAssistant } from "../../types";
@@ -35,28 +36,28 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
)}
</p>
</div>
<ha-md-list>
<ha-list-base>
${this.assistConfiguration!.available_wake_words.map(
(wakeWord) =>
html`<ha-md-list-item
interactive
type="button"
html`<ha-list-item-button
@click=${this._wakeWordPicked}
.value=${wakeWord.id}
>
${wakeWord.wake_word}
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>`
</ha-list-item-button>`
)}
</ha-md-list>`;
</ha-list-base>`;
}
private async _wakeWordPicked(ev) {
private async _wakeWordPicked(ev: Event) {
if (!this.assistEntityId) {
return;
}
const wakeWordId = ev.currentTarget.value;
const wakeWordId = (
ev.currentTarget as HaListItemButton & { value: string }
).value;
await setWakeWords(this.hass, this.assistEntityId, [wakeWordId]);
this._nextStep();
@@ -75,7 +76,7 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
.padding {
padding: 24px;
}
ha-md-list {
ha-list-base {
width: 100%;
text-align: initial;
margin-bottom: 24px;
@@ -363,9 +363,6 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
static styles = [
AssistantSetupStyles,
css`
ha-md-list-item {
text-align: initial;
}
ha-tts-voice-picker {
display: block;
}
+5 -5
View File
@@ -56,7 +56,7 @@ export class HomeAssistantMain extends LitElement {
.type=${sidebarNarrow ? "modal" : ""}
.open=${sidebarNarrow ? this._drawerOpen : false}
.direction=${computeRTLDirection(this.hass)}
@MDCDrawer:closed=${this._drawerClosed}
@hass-drawer-closed=${this._drawerClosed}
>
<ha-sidebar
.hass=${this.hass}
@@ -152,16 +152,16 @@ export class HomeAssistantMain extends LitElement {
color: var(--primary-text-color);
/* remove the grey tap highlights in iOS on the fullscreen touch targets */
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
--mdc-drawer-width: calc(56px + var(--safe-area-inset-left, 0px));
--mdc-top-app-bar-width: calc(100% - var(--mdc-drawer-width));
--ha-sidebar-width: calc(56px + var(--safe-area-inset-left, 0px));
--mdc-top-app-bar-width: calc(100% - var(--ha-sidebar-width));
--safe-area-content-inset-left: 0px;
--safe-area-content-inset-right: var(--safe-area-inset-right);
}
:host([expanded]) {
--mdc-drawer-width: calc(256px + var(--safe-area-inset-left, 0px));
--ha-sidebar-width: calc(256px + var(--safe-area-inset-left, 0px));
}
:host([modal]) {
--mdc-drawer-width: unset;
--ha-sidebar-width: unset;
--mdc-top-app-bar-width: unset;
--safe-area-content-inset-left: var(--safe-area-inset-left);
}
+13 -14
View File
@@ -5,9 +5,9 @@ import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-button";
import "../components/ha-icon-button-next";
import "../components/ha-md-list";
import "../components/ha-md-list-item";
import "../components/ha-icon-next";
import "../components/item/ha-list-item-button";
import "../components/list/ha-list-base";
import type { HomeAssistant } from "../types";
import { onBoardingStyles } from "./styles";
@@ -37,8 +37,8 @@ class OnboardingWelcome extends LitElement {
</div>
</div>
<ha-md-list>
<ha-md-list-item type="button" @click=${this._restoreBackupUpload}>
<ha-list-base>
<ha-list-item-button @click=${this._restoreBackupUpload}>
<div slot="headline">
${this.localize("ui.panel.page-onboarding.restore.upload_backup")}
</div>
@@ -47,18 +47,18 @@ class OnboardingWelcome extends LitElement {
"ui.panel.page-onboarding.restore.options.upload_description"
)}
</div>
<ha-icon-button-next slot="end"></ha-icon-button-next>
</ha-md-list-item>
<ha-md-list-item type="button" @click=${this._restoreBackupCloud}>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button @click=${this._restoreBackupCloud}>
<div slot="headline">Home Assistant Cloud</div>
<div slot="supporting-text">
${this.localize(
"ui.panel.page-onboarding.restore.ha-cloud.description"
)}
</div>
<ha-icon-button-next slot="end"></ha-icon-button-next>
</ha-md-list-item>
</ha-md-list>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-list-base>
`;
}
@@ -123,11 +123,10 @@ class OnboardingWelcome extends LitElement {
padding: 0 var(--ha-space-4);
}
ha-md-list {
ha-list-base {
width: 100%;
padding-bottom: 0;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--ha-row-item-padding-inline: 0;
}
`,
];
@@ -8,9 +8,8 @@ import type { HaProgressButton } from "../../components/buttons/ha-progress-butt
import "../../components/ha-alert";
import "../../components/ha-button";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import "../../components/input/ha-input";
import "../../components/item/ha-row-item";
import {
getPreferredAgentForDownload,
type BackupContentExtended,
@@ -92,33 +91,30 @@ class OnboardingRestoreBackupRestore extends LitElement {
</ha-alert>`
: nothing}
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.localize(
"ui.panel.page-onboarding.restore.details.summary.created"
)}
</span>
<span slot="supporting-text">${formattedDate}</span>
</ha-md-list-item>
${onlyHomeAssistantBackup
? html`<ha-md-list-item>
<span slot="headline">
${this.localize(
"ui.panel.page-onboarding.restore.details.summary.content"
)}
</span>
<ha-backup-formfield-label
slot="supporting-text"
.version=${this.backup.homeassistant_version}
.label=${this.localize(
`ui.panel.page-onboarding.restore.data_picker.${this.backup.database_included ? "settings_and_history" : "settings"}`
)}
></ha-backup-formfield-label>
</ha-md-list-item>`
: nothing}
</ha-md-list>
<ha-row-item>
<span slot="headline">
${this.localize(
"ui.panel.page-onboarding.restore.details.summary.created"
)}
</span>
<span slot="supporting-text">${formattedDate}</span>
</ha-row-item>
${onlyHomeAssistantBackup
? html`<ha-row-item>
<span slot="headline">
${this.localize(
"ui.panel.page-onboarding.restore.details.summary.content"
)}
</span>
<ha-backup-formfield-label
slot="supporting-text"
.version=${this.backup.homeassistant_version}
.label=${this.localize(
`ui.panel.page-onboarding.restore.data_picker.${this.backup.database_included ? "settings_and_history" : "settings"}`
)}
></ha-backup-formfield-label>
</ha-row-item>`
: nothing}
${!onlyHomeAssistantBackup
? html`<h2>
${this.localize("ui.panel.page-onboarding.restore.select_type")}
@@ -312,26 +308,8 @@ class OnboardingRestoreBackupRestore extends LitElement {
display: block;
margin-top: 16px;
}
ha-md-list {
background: none;
padding: 0;
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-list-item-two-line-container-height: 64px;
--md-list-item-supporting-text-size: 1rem;
--md-list-item-label-text-size: 0.875rem;
--md-list-item-label-text-color: var(--secondary-text-color);
--md-list-item-supporting-text-color: var(--primary-text-color);
}
ha-md-list-item [slot="supporting-text"] {
display: flex;
align-items: center;
flex-direction: row;
gap: var(--ha-space-2);
line-height: var(--ha-line-height-condensed);
ha-row-item {
--ha-row-item-padding-inline: 0;
}
h2 {
font-size: var(--ha-font-size-xl);
@@ -13,13 +13,12 @@ import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-spinner";
import "../../../../../components/ha-faded";
import "../../../../../components/ha-markdown";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-spinner";
import "../../../../../components/ha-switch";
import type { HaSwitch } from "../../../../../components/ha-switch";
import "../../../../../components/item/ha-row-item";
import type { HassioAddonDetails } from "../../../../../data/hassio/addon";
import {
fetchHassioAddonChangelog,
@@ -108,25 +107,20 @@ class SupervisorAppUpdateAvailableCard extends LitElement {
${createBackupTexts
? html`
<hr />
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${createBackupTexts.title}
</span>
<ha-row-item>
<span slot="headline">
${createBackupTexts.title}
</span>
${createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing}
<ha-switch
slot="end"
id="create-backup"
></ha-switch>
</ha-md-list-item>
</ha-md-list>
${createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing}
<ha-switch slot="end" id="create-backup"></ha-switch>
</ha-row-item>
`
: nothing}
`
@@ -273,16 +267,10 @@ class SupervisorAppUpdateAvailableCard extends LitElement {
margin: var(--ha-space-4) 0 0 0;
}
ha-md-list {
padding: 0;
ha-row-item {
--ha-row-item-padding-inline: 0;
margin-bottom: calc(-1 * var(--ha-space-4));
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-item-overflow: visible;
}
`,
];
}
@@ -1,7 +1,7 @@
import "@home-assistant/webawesome/dist/components/tag/tag";
import {
mdiCheckCircle,
mdiChip,
mdiCircleOffOutline,
mdiCursorDefaultClickOutline,
mdiDocker,
mdiExclamationThick,
@@ -17,11 +17,10 @@ import {
mdiNumeric6,
mdiNumeric7,
mdiNumeric8,
mdiPlayCircle,
mdiPound,
mdiShield,
} from "@mdi/js";
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -31,6 +30,7 @@ import { atLeastVersion } from "../../../../../common/config/version";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { navigate } from "../../../../../common/navigate";
import { capitalizeFirstLetter } from "../../../../../common/string/capitalize-first-letter";
import type { LocalizeKeys } from "../../../../../common/translations/localize";
import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/chips/ha-assist-chip";
import "../../../../../components/chips/ha-chip-set";
@@ -79,9 +79,9 @@ import { bytesToString } from "../../../../../util/bytes-to-string";
import { getAppDisplayName } from "../../common/app";
import "../../components/supervisor-apps-card-content";
import "../components/supervisor-app-metric";
import "../components/supervisor-app-update-available-card";
import { extractChangelog } from "../util/supervisor-app";
import "./supervisor-app-system-managed";
import "../components/supervisor-app-update-available-card";
const STAGE_ICON = {
stable: mdiCheckCircle,
@@ -203,28 +203,10 @@ class SupervisorAppInfo extends LitElement {
: nothing}
<div class="addon-version light-color">
${this.addon.version
? html`
${this._computeIsRunning
? html`
<ha-svg-icon
.title=${this.hass.localize(
"ui.panel.config.apps.dashboard.app_running"
)}
class="running"
.path=${mdiPlayCircle}
></ha-svg-icon>
`
: html`
<ha-svg-icon
.title=${this.hass.localize(
"ui.panel.config.apps.dashboard.app_stopped"
)}
class="stopped"
.path=${mdiCircleOffOutline}
></ha-svg-icon>
`}
`
: html` ${this.addon.version_latest} `}
? html`<supervisor-apps-state
.state=${this.addon.state}
></supervisor-apps-state>`
: this.addon.version_latest}
</div>
</div>
<div class="description light-color">
@@ -837,7 +819,7 @@ class SupervisorAppInfo extends LitElement {
const id = ev.currentTarget.id as AddonCapability;
showAlertDialog(this, {
title: this.hass.localize(
`ui.panel.config.apps.dashboard.capability.${id}.title`
`ui.panel.config.apps.dashboard.capability.${id}.title` as LocalizeKeys
),
text: this.hass.localize(
`ui.panel.config.apps.dashboard.capability.${id}.description`
@@ -1,11 +1,20 @@
import "@home-assistant/webawesome/dist/components/tag/tag";
import { mdiHelpCircleOutline } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../components/ha-svg-icon";
import type { AddonStage } from "../../../../data/hassio/addon";
import type { AddonStage, AddonState } from "../../../../data/hassio/addon";
import type { HomeAssistant } from "../../../../types";
import { getAppDisplayName } from "../common/app";
import "./supervisor-apps-state";
import "./supervisor-apps-tag";
export interface AppTag {
label: string;
variant: "brand" | "success" | "warning" | "danger" | "neutral";
iconPath?: string;
}
@customElement("supervisor-apps-card-content")
class SupervisorAppsCardContent extends LitElement {
@@ -16,13 +25,13 @@ class SupervisorAppsCardContent extends LitElement {
@property() public stage: AddonStage = "stable";
@property() public state: AddonState = null;
@property() public description?: string;
@property({ type: Boolean }) public available = true;
@property({ attribute: false }) public showTopbar = false;
@property({ attribute: false }) public topbarClass?: string;
@property({ attribute: false }) public tags?: AppTag[];
@property({ attribute: false }) public iconTitle?: string;
@@ -33,78 +42,87 @@ class SupervisorAppsCardContent extends LitElement {
@property({ attribute: false }) public iconImage?: string;
protected render(): TemplateResult {
const stageLabel =
this.stage !== "stable"
? this.hass.localize(
`ui.panel.config.apps.dashboard.capability.stages.${this.stage}`
)
: undefined;
return html`
${this.showTopbar
? html` <div class="topbar ${this.topbarClass}"></div> `
: ""}
${this.iconImage
? html`
<div class="icon_image ${this.iconClass}">
<img
src=${this.iconImage}
.title=${this.iconTitle}
alt=${this.iconTitle ?? ""}
/>
<div></div>
</div>
`
: html`
<ha-svg-icon
class=${this.iconClass!}
.path=${this.icon}
.title=${this.iconTitle}
></ha-svg-icon>
`}
<div>
<div class="title-row">
<div class="title">${getAppDisplayName(this.title, this.stage)}</div>
${stageLabel
? html` <span class="stage ${this.stage}"> ${stageLabel} </span> `
: nothing}
<div class="app">
<div class="icon-wrapper">
${this.iconImage
? html`
<img
class="icon-image"
src=${this.iconImage}
.title=${this.iconTitle}
alt=${this.iconTitle ?? ""}
/>
`
: html`
<ha-svg-icon
class="app-icon"
.path=${this.icon}
.title=${this.iconTitle}
></ha-svg-icon>
`}
</div>
<div class="addition">
${this.description}
${
/* treat as available when undefined */
this.available === false ? " (Not available)" : ""
}
<div>
<div class="title-row">
<div class="title">
${getAppDisplayName(this.title, this.stage)}
</div>
</div>
<div class="addition">
${this.description}
${
/* treat as available when undefined */
this.available === false ? " (Not available)" : ""
}
</div>
</div>
</div>
${this.tags?.length || this.state
? html`
<div class="footer">
<supervisor-apps-state
.state=${this.state || "unknown"}
></supervisor-apps-state>
${this.tags?.length
? html`<div class="tags">
${this.tags.map(
(tag) =>
html`<supervisor-apps-tag
.variant=${tag.variant}
.iconPath=${tag.iconPath}
.label=${tag.label}
></supervisor-apps-tag>`
)}
</div>`
: nothing}
</div>
`
: nothing}
`;
}
static styles = css`
:host {
direction: ltr;
.app {
margin-bottom: var(--ha-space-2);
gap: var(--ha-space-4);
display: flex;
}
ha-svg-icon {
margin-right: var(--ha-space-6);
.icon-wrapper {
position: relative;
margin-top: var(--ha-space-1);
width: 40px;
height: 40px;
flex-shrink: 0;
}
.app-icon {
margin-left: var(--ha-space-2);
margin-top: var(--ha-space-3);
float: left;
margin-top: var(--ha-space-2);
color: var(--secondary-text-color);
}
ha-svg-icon.update {
color: var(--warning-color);
}
ha-svg-icon.running,
ha-svg-icon.installed {
color: var(--success-color);
}
ha-svg-icon.hassupdate,
ha-svg-icon.backup {
color: var(--state-icon-color);
}
ha-svg-icon.not_available {
color: var(--error-color);
.icon-image {
max-height: 40px;
max-width: 40px;
}
.title {
flex: 1;
@@ -120,22 +138,6 @@ class SupervisorAppsCardContent extends LitElement {
gap: var(--ha-space-2);
min-width: 0;
}
.stage {
flex: none;
border-radius: 999px;
font-size: 12px;
line-height: 1;
padding: 4px 8px;
white-space: nowrap;
}
.stage.experimental {
color: var(--warning-color);
background-color: rgba(var(--rgb-warning-color), 0.12);
}
.stage.deprecated {
color: var(--error-color);
background-color: rgba(var(--rgb-error-color), 0.12);
}
.addition {
color: var(--secondary-text-color);
margin-top: var(--ha-space-1);
@@ -144,43 +146,18 @@ class SupervisorAppsCardContent extends LitElement {
height: 2.4em;
line-height: var(--ha-line-height-condensed);
}
.icon_image img {
max-height: 40px;
max-width: 40px;
margin-top: var(--ha-space-1);
margin-right: var(--ha-space-4);
float: left;
.footer {
border-top: var(--ha-border-width-sm) solid
var(--ha-color-border-neutral-quiet);
padding-top: var(--ha-space-2);
display: flex;
gap: var(--ha-space-2);
flex-wrap: wrap;
justify-content: space-between;
}
.icon_image.stopped,
.icon_image.not_available {
filter: grayscale(1);
}
.dot {
position: absolute;
background-color: var(--warning-color);
width: 12px;
height: 12px;
top: var(--ha-space-2);
right: var(--ha-space-2);
border-radius: var(--ha-border-radius-circle);
}
.topbar {
position: absolute;
width: 100%;
height: 2px;
top: 0;
left: 0;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.topbar.installed {
background-color: var(--primary-color);
}
.topbar.update {
background-color: var(--accent-color);
}
.topbar.unavailable {
background-color: var(--error-color);
.tags {
display: flex;
gap: var(--ha-space-2);
}
`;
}
@@ -0,0 +1,79 @@
import { consume, type ContextType } from "@lit/context";
import { mdiHelpCircle } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../components/ha-svg-icon";
import { internationalizationContext } from "../../../../data/context";
import type { AddonState } from "../../../../data/hassio/addon";
@customElement("supervisor-apps-state")
class SupervisorAppsState extends LitElement {
@property() public state: Exclude<AddonState, null> = "unknown";
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
protected render(): TemplateResult {
return html`
${this.state === "unknown"
? html`<ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon>`
: html` <div class="dot state-${this.state}"></div> `}
<span
>${this._i18n.localize(
`ui.panel.config.apps.dashboard.capability.state.${this.state}`
)}</span
>
`;
}
static styles = css`
:host {
display: inline-flex;
align-items: center;
gap: var(--ha-space-2);
color: var(--ha-color-text-secondary);
font-size: var(--ha-font-size-m);
}
.dot {
width: 8px;
height: 8px;
border-radius: var(--ha-border-radius-circle);
background-color: var(--ha-color-on-neutral-normal);
flex-shrink: 0;
}
.dot.state-started {
background-color: var(--ha-color-green-80);
animation: state-dot-pulse 1.8s infinite;
}
.dot.state-startup {
background-color: var(--ha-color-on-warning-normal);
}
.dot.state-error {
background-color: var(--ha-color-on-danger-normal);
}
ha-svg-icon {
--mdc-icon-size: 20px;
}
@keyframes state-dot-pulse {
0% {
box-shadow: 0 0 0 0 rgba(var(--rgb-success-color), 0.6);
}
100% {
box-shadow: 0 0 0 6px rgba(var(--rgb-success-color), 0);
}
}
@media (prefers-reduced-motion) {
.dot.state-started {
animation: none;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"supervisor-apps-state": SupervisorAppsState;
}
}
@@ -0,0 +1,64 @@
import "@home-assistant/webawesome/dist/components/tag/tag";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../components/ha-svg-icon";
@customElement("supervisor-apps-tag")
class SupervisorAppsTag extends LitElement {
@property() public variant:
| "brand"
| "success"
| "warning"
| "danger"
| "neutral" = "neutral";
@property({ attribute: "icon-path" }) public iconPath?: string;
@property() public label!: string;
protected render(): TemplateResult {
return html`<wa-tag .variant=${this.variant}>
${this.iconPath
? html`<ha-svg-icon .path=${this.iconPath}></ha-svg-icon>`
: nothing}
${this.label}
</wa-tag>`;
}
static styles = css`
wa-tag {
font-size: var(--ha-font-size-xs);
border-radius: var(--ha-border-radius-pill);
height: 20px;
border: none;
padding-inline: var(--ha-space-1) var(--ha-space-2);
}
wa-tag ha-svg-icon {
--mdc-icon-size: 16px;
width: 16px;
height: 16px;
}
wa-tag[variant="success"] {
color: var(--ha-color-on-success-normal);
}
wa-tag[variant="warning"] {
color: var(--ha-color-on-warning-normal);
}
wa-tag[variant="danger"] {
color: var(--ha-color-on-error-normal);
}
wa-tag[variant="neutral"] {
color: var(--ha-color-on-neutral-normal);
}
wa-tag[variant="brand"] {
color: var(--ha-color-on-primary-normal);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"supervisor-apps-tag": SupervisorAppsTag;
}
}
@@ -1,5 +1,8 @@
import {
mdiAlertDecagramOutline,
mdiArrowUpBoldCircle,
mdiArrowUpBoldCircleOutline,
mdiFlask,
mdiPuzzle,
mdiRefresh,
mdiStorePlus,
@@ -29,7 +32,9 @@ import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-subpage";
import type { HomeAssistant, Route } from "../../../types";
import { getAppDisplayName } from "./common/app";
import "./components/supervisor-apps-card-content";
import type { AppTag } from "./components/supervisor-apps-card-content";
import { supervisorAppsStyle } from "./resources/supervisor-apps-style";
@customElement("ha-config-apps-installed")
@@ -96,65 +101,59 @@ export class HaConfigAppsInstalled extends LitElement {
</ha-input-search>
</div>
<div class="content">
<div class="card-group">
${addons.length === 0
? html`
<ha-card outlined>
${addons.length === 0
? html`
<ha-card outlined>
<div class="card-content">
<button class="link" @click=${this._openStore}>
${this.hass.localize(
"ui.panel.config.apps.installed.no_apps"
)}
</button>
</div>
</ha-card>
`
: addons.map(
(addon) => html`
<ha-card
role="button"
tabindex="0"
outlined
.addon=${addon}
@click=${this._addonTapped}
aria-label=${getAppDisplayName(addon.name, addon.stage)}
>
<div class="card-content">
<button class="link" @click=${this._openStore}>
${this.hass.localize(
"ui.panel.config.apps.installed.no_apps"
)}
</button>
<supervisor-apps-card-content
.hass=${this.hass}
.title=${addon.name}
.stage=${addon.stage}
.description=${addon.description}
available
.tags=${this._getAppTags(addon)}
.state=${addon.state}
.icon=${addon.update_available
? mdiArrowUpBoldCircle
: mdiPuzzle}
.iconTitle=${addon.state !== "started"
? this.hass.localize(
"ui.panel.config.apps.installed.app_stopped"
)
: addon.update_available
? this.hass.localize(
"ui.panel.config.apps.installed.app_update_available"
)
: this.hass.localize(
"ui.panel.config.apps.installed.app_running"
)}
.iconImage=${addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined}
></supervisor-apps-card-content>
</div>
</ha-card>
`
: addons.map(
(addon) => html`
<ha-card
outlined
.addon=${addon}
@click=${this._addonTapped}
>
<div class="card-content">
<supervisor-apps-card-content
.hass=${this.hass}
.title=${addon.name}
.stage=${addon.stage}
.description=${addon.description}
available
.showTopbar=${addon.update_available}
topbarClass="update"
.icon=${addon.update_available
? mdiArrowUpBoldCircle
: mdiPuzzle}
.iconTitle=${addon.state !== "started"
? this.hass.localize(
"ui.panel.config.apps.installed.app_stopped"
)
: addon.update_available
? this.hass.localize(
"ui.panel.config.apps.installed.app_update_available"
)
: this.hass.localize(
"ui.panel.config.apps.installed.app_running"
)}
.iconClass=${addon.update_available
? addon.state === "started"
? "update"
: "update stopped"
: addon.state === "started"
? "running"
: "stopped"}
.iconImage=${addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined}
></supervisor-apps-card-content>
</div>
</ha-card>
`
)}
</div>
)}
</div>
<ha-button size="large" href="/config/apps/available">
@@ -217,6 +216,32 @@ export class HaConfigAppsInstalled extends LitElement {
navigate("/config/apps/available");
}
private _getAppTags(addon: HassioAddonInfo): AppTag[] {
const labels: AppTag[] = [];
if (addon.update_available) {
labels.push({
label: this.hass.localize(
`ui.panel.config.apps.state.update_available`
),
variant: "brand",
iconPath: mdiArrowUpBoldCircleOutline,
});
}
if (addon.stage !== "stable") {
labels.push({
label: this.hass.localize(
`ui.panel.config.apps.dashboard.capability.stages.${addon.stage}`
),
variant: addon.stage === "experimental" ? "warning" : "danger",
iconPath:
addon.stage === "experimental" ? mdiFlask : mdiAlertDecagramOutline,
});
}
return labels;
}
static styles: CSSResultGroup = [
supervisorAppsStyle,
css`
@@ -229,7 +254,10 @@ export class HaConfigAppsInstalled extends LitElement {
ha-card {
cursor: pointer;
overflow: hidden;
direction: ltr;
}
ha-card:hover {
background-color: var(--ha-color-fill-neutral-quiet-resting);
}
.search {
@@ -247,10 +275,13 @@ export class HaConfigAppsInstalled extends LitElement {
.content {
padding: var(--ha-space-4);
margin-bottom: var(--ha-space-18);
gap: var(--ha-space-4);
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(336px, 100%), 1fr));
}
.card-content {
padding: var(--ha-space-4);
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
}
button.link {
@@ -71,7 +71,6 @@ export default class HaAutomationActionEditor extends LitElement {
`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.action}
@value-changed=${this._onYamlChange}
.readOnly=${this.disabled}
@@ -54,7 +54,6 @@ export class HaEventAction extends LitElement implements ActionElement {
@change=${this._eventChanged}
></ha-input>
<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.event.event_data"
)}
@@ -85,7 +85,6 @@ export class HaServiceAction extends LitElement implements ActionElement {
.hass=${this.hass}
.value=${this._action}
.disabled=${this.disabled}
.showAdvanced=${this.hass.userData?.showAdvanced}
.hidePicker=${!!this._action.metadata}
@value-changed=${this._actionChanged}
></ha-service-control>
@@ -47,8 +47,6 @@ import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-button-prev";
import "../../../components/ha-icon-next";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box";
import "../../../components/ha-section-title";
import "../../../components/ha-service-icon";
@@ -56,6 +54,8 @@ import "../../../components/ha-tooltip";
import { TRIGGER_ICONS } from "../../../components/ha-trigger-icon";
import "../../../components/input/ha-input-search";
import type { HaInputSearch } from "../../../components/input/ha-input-search";
import "../../../components/item/ha-list-item-button";
import "../../../components/list/ha-list-base";
import {
ACTION_BUILDING_BLOCKS_GROUP,
ACTION_COLLECTIONS,
@@ -731,7 +731,7 @@ class DialogAddAutomationElement
.manifests=${this._manifests}
></ha-automation-add-from-target>`
: html`
<ha-md-list
<ha-list-base
class=${classMap({
groups: true,
hidden: hideCollections,
@@ -739,9 +739,7 @@ class DialogAddAutomationElement
})}
>
${this._params!.clipboardItem
? html`<ha-md-list-item
interactive
type="button"
? html`<ha-list-item-button
class="paste"
@click=${this._paste}
>
@@ -785,7 +783,7 @@ class DialogAddAutomationElement
slot="end"
.path=${mdiPlus}
></ha-svg-icon>
</ha-md-list-item>
</ha-list-item-button>
<wa-divider></wa-divider>`
: nothing}
${collections.map(
@@ -799,9 +797,7 @@ class DialogAddAutomationElement
collection.groups,
(item) => item.key,
(item) => html`
<ha-md-list-item
interactive
type="button"
<ha-list-item-button
.value=${item.key}
.index=${collection.collectionIndex}
@click=${this._groupSelected}
@@ -821,12 +817,12 @@ class DialogAddAutomationElement
${this._narrow
? html`<ha-icon-next slot="end"></ha-icon-next>`
: nothing}
</ha-md-list-item>
</ha-list-item-button>
`
)}
`
)}
</ha-md-list>
</ha-list-base>
`}
${!this._filter
? html`
@@ -2391,8 +2387,14 @@ class DialogAddAutomationElement
gap: var(--ha-space-3);
}
ha-md-list {
padding: 0;
ha-list-item-button {
--ha-row-item-padding-block: var(--ha-space-1);
--ha-row-item-padding-inline: var(--ha-space-3);
--ha-row-item-min-height: 40px;
}
ha-list-item-button::part(start),
ha-list-item-button::part(end) {
color: var(--ha-color-on-neutral-quiet);
}
ha-automation-add-from-target,
@@ -23,11 +23,12 @@ import { stringCompare } from "../../../../common/string/compare";
import "../../../../components/ha-floor-icon";
import "../../../../components/ha-icon";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-section-title";
import "../../../../components/ha-state-icon";
import "../../../../components/ha-svg-icon";
import "../../../../components/item/ha-list-item-button";
import "../../../../components/item/ha-row-item";
import "../../../../components/list/ha-list-base";
import {
getAreaDeviceLookup,
getAreaEntityLookup,
@@ -328,15 +329,13 @@ export default class HaAutomationAddFromTarget extends LitElement {
)}</ha-section-title
>
${emptyFloors
? html`<ha-md-list>
<ha-md-list-item type="text">
<div slot="headline">
${this._i18n.localize("ui.components.area-picker.no_areas")}
</div>
</ha-md-list-item>
</ha-md-list>`
? html`<ha-row-item>
<div slot="headline">
${this._i18n.localize("ui.components.area-picker.no_areas")}
</div>
</ha-row-item>`
: html`${narrow
? html`<ha-md-list>${floorAreas}</ha-md-list>`
? html`<ha-list-base>${floorAreas}</ha-list-base>`
: html`<wa-tree
@wa-selection-change=${this._handleSelectionChange}
>${floorAreas}</wa-tree
@@ -370,12 +369,10 @@ export default class HaAutomationAddFromTarget extends LitElement {
"ui.components.label-picker.labels"
)}</ha-section-title
>
<ha-md-list>
<ha-list-base>
${labels.map(
(label) =>
html`<ha-md-list-item
interactive
type="button"
html`<ha-list-item-button
.target=${label.id}
@click=${this._selectItem}
class=${this._getSelectedTargetId(value) === label.id
@@ -393,9 +390,9 @@ export default class HaAutomationAddFromTarget extends LitElement {
${narrow
? html`<ha-icon-next slot="end"></ha-icon-next> `
: nothing}
</ha-md-list-item>`
</ha-list-item-button>`
)}
</ha-md-list>`;
</ha-list-base>`;
}
);
@@ -514,7 +511,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
"ui.panel.config.automation.editor.unassigned"
)}</ha-section-title
>${narrow
? html`<ha-md-list>${items}</ha-md-list>`
? html`<ha-list-base>${items}</ha-list-base>`
: html`<wa-tree @wa-selection-change=${this._handleSelectionChange}>
${items}
</wa-tree>`} `;
@@ -568,7 +565,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
"ui.components.target-picker.type.areas"
)}</ha-section-title
>
<ha-md-list>${renderedAreas}</ha-md-list>`;
<ha-list-base>${renderedAreas}</ha-list-base>`;
}
return renderedAreas;
@@ -617,7 +614,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
"ui.components.target-picker.type.devices"
)}</ha-section-title
>
<ha-md-list>${renderedDevices}</ha-md-list>`;
<ha-list-base>${renderedDevices}</ha-list-base>`;
}
return renderedDevices;
@@ -664,7 +661,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
"ui.components.target-picker.type.devices"
)}</ha-section-title
>
<ha-md-list> ${renderedDomains} </ha-md-list>`;
<ha-list-base>${renderedDomains}</ha-list-base>`;
}
return renderedDomains;
@@ -719,7 +716,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
"ui.components.target-picker.type.entities"
)}</ha-section-title
>
<ha-md-list>${renderedEntites}</ha-md-list>`;
<ha-list-base>${renderedEntites}</ha-list-base>`;
}
return renderedEntites;
@@ -784,17 +781,14 @@ export default class HaAutomationAddFromTarget extends LitElement {
children?: TemplateResult | TemplateResult[] | typeof nothing
) {
if (this.narrow) {
return html`<ha-md-list-item
interactive
type="button"
return html`<ha-list-item-button
.target=${target}
@click=${this._selectItem}
.title=${label}
>
${icon?.("start")}
<div slot="headline">${label}</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>`;
</ha-list-item-button>`;
}
return html`
@@ -1191,7 +1185,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
await this.updateComplete;
if (type === "label") {
this.shadowRoot!.querySelector(
"ha-md-list-item.selected"
"ha-list-item-button.selected"
)?.scrollIntoView({
block: "center",
});
@@ -1416,9 +1410,9 @@ export default class HaAutomationAddFromTarget extends LitElement {
font-weight: var(--ha-font-weight-medium);
overflow: hidden;
}
ha-md-list-item {
--md-list-item-label-text-weight: var(--ha-font-weight-medium);
--md-list-item-label-text-font: var(--ha-font-family-heading);
ha-list-item-button::part(label) {
font-weight: var(--ha-font-weight-medium);
font-family: var(--ha-font-family-heading);
}
.item-label {
@@ -1462,29 +1456,33 @@ export default class HaAutomationAddFromTarget extends LitElement {
background-color: yellow;
}
ha-md-list {
padding: 0;
--md-list-item-leading-space: var(--ha-space-3);
--md-list-item-trailing-space: var(--md-list-item-leading-space);
--md-list-item-bottom-space: var(--ha-space-1);
--md-list-item-top-space: var(--md-list-item-bottom-space);
--md-list-item-supporting-text-font: var(--ha-font-size-s);
--md-list-item-one-line-container-height: var(--ha-space-10);
ha-list-base {
--ha-row-item-padding-inline: var(--ha-space-3);
--ha-row-item-padding-block: var(--ha-space-1);
--ha-row-item-min-height: 40px;
}
ha-md-list-item.selected {
ha-list-item-button::part(end) {
color: var(--ha-color-on-neutral-quiet);
}
ha-list-item-button.selected {
background-color: var(--ha-color-fill-primary-normal-active);
--md-list-item-label-text-color: var(--ha-color-on-primary-normal);
--icon-primary-color: var(--ha-color-on-primary-normal);
}
ha-list-item-button.selected::part(headline) {
color: var(--ha-color-on-primary-normal);
}
wa-tree-item[selected],
wa-tree-item[selected] > ha-svg-icon,
wa-tree-item[selected] > ha-icon,
wa-tree-item[selected] > ha-state-icon,
wa-tree-item[selected] > ha-floor-icon,
ha-md-list-item.selected ha-icon,
ha-md-list-item.selected ha-svg-icon {
ha-list-item-button.selected ha-icon,
ha-list-item-button.selected ha-svg-icon {
color: var(--ha-color-on-primary-normal);
}
@@ -12,15 +12,15 @@ import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tooltip";
import "../../../../components/item/ha-list-item-button";
import "../../../../components/list/ha-list-base";
import type { ConfigEntry } from "../../../../data/config_entries";
import type { LabelRegistryEntry } from "../../../../data/label/label_registry";
import { haStyleScrollbar } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type { AddAutomationElementListItem } from "../add-automation-element-dialog";
import { haStyleScrollbar } from "../../../../resources/styles";
import { getTargetIcon } from "../target/get_target_icon";
type Target = [string, string | undefined, string | undefined];
@@ -103,17 +103,12 @@ export class HaAutomationAddItems extends LitElement {
return html`
<div class="items-title">${title}</div>
<ha-md-list>
<ha-list-base>
${repeat(
items,
(item) => item.key,
(item) => html`
<ha-md-list-item
interactive
type="button"
.value=${item.key}
@click=${this._selected}
>
<ha-list-item-button .value=${item.key} @click=${this._selected}>
<div slot="headline" class=${this.target ? "item-headline" : ""}>
${item.name}${this._renderTarget(this.target)}
</div>
@@ -138,6 +133,7 @@ export class HaAutomationAddItems extends LitElement {
@click=${stopPropagation}
></ha-svg-icon>
<ha-tooltip
slot="end"
.for=${`description-tooltip-${item.key}`}
@wa-show=${stopPropagation}
@wa-hide=${stopPropagation}
@@ -151,10 +147,10 @@ export class HaAutomationAddItems extends LitElement {
class="plus"
.path=${mdiPlus}
></ha-svg-icon>
</ha-md-list-item>
</ha-list-item-button>
`
)}
</ha-md-list>
</ha-list-base>
`;
}
@@ -245,24 +241,26 @@ export class HaAutomationAddItems extends LitElement {
background-color: var(--ha-color-fill-danger-quiet-resting);
color: var(--ha-color-on-danger-normal);
}
.items ha-md-list {
--md-list-item-two-line-container-height: var(--ha-space-12);
--md-list-item-leading-space: var(--ha-space-3);
--md-list-item-trailing-space: var(--md-list-item-leading-space);
--md-list-item-bottom-space: var(--ha-space-2);
--md-list-item-top-space: var(--md-list-item-bottom-space);
--md-list-item-supporting-text-font: var(--ha-font-family-body);
--ha-md-list-item-gap: var(--ha-space-3);
.items ha-list-base {
--ha-row-item-padding-inline: var(--ha-space-3);
--ha-row-item-padding-block: var(--ha-space-2);
--ha-list-gap: var(--ha-space-3);
gap: var(--ha-space-2);
padding: 0 var(--ha-space-4);
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-3));
}
.items ha-md-list ha-md-list-item {
.items ha-list-base ha-list-item-button {
border-radius: var(--ha-border-radius-lg);
border: 1px solid var(--ha-color-border-neutral-quiet);
overflow: hidden;
}
.items ha-md-list {
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-3));
.items ha-list-base ha-list-item-button::part(start),
.items ha-list-base ha-list-item-button::part(end) {
color: var(--ha-color-on-neutral-quiet);
}
.items ha-list-base ha-list-item-button::part(end) {
gap: var(--ha-space-3);
}
.items .item-headline {
@@ -8,8 +8,6 @@ import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-select-box";
import "../../../../components/input/ha-input";
@@ -79,7 +79,6 @@ export default class HaAutomationConditionEditor extends LitElement {
`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.condition}
@value-changed=${this._onYamlChange}
.readOnly=${this.disabled}
@@ -186,7 +186,6 @@ export class HaPlatformCondition extends LitElement {
: nothing}
${shouldRenderDataYaml
? html`<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.components.service-control.action_data"
)}
@@ -541,7 +541,6 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._preprocessYaml()}
.readOnly=${this.readOnly}
@value-changed=${this._yamlChanged}
@@ -276,7 +276,6 @@ export class HaAutomationTrace extends LitElement {
: this._view === "automation_config"
? html`
<ha-trace-config
.hass=${this.hass}
.trace=${this._trace}
></ha-trace-config>
`
@@ -292,7 +291,6 @@ export class HaAutomationTrace extends LitElement {
: this._view === "blueprint"
? html`
<ha-trace-blueprint-config
.hass=${this.hass}
.trace=${this._trace}
></ha-trace-blueprint-config>
`
@@ -52,7 +52,6 @@ class DialogPasteReplace extends LitElement {
</p>
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._params?.pastedConfig}
read-only
></ha-yaml-editor>
+2 -2
View File
@@ -124,9 +124,9 @@ export const manualEditorStyles = css`
.has-sidebar {
--sidebar-width: min(
max(var(--sidebar-dynamic-width), ${SIDEBAR_MIN_WIDTH}px),
100vw - ${CONTENT_MIN_WIDTH}px - var(--mdc-drawer-width, 0px),
100vw - ${CONTENT_MIN_WIDTH}px - var(--ha-sidebar-width, 0px),
var(--ha-automation-editor-max-width) -
${CONTENT_MIN_WIDTH}px - var(--mdc-drawer-width, 0px)
${CONTENT_MIN_WIDTH}px - var(--ha-sidebar-width, 0px)
);
--sidebar-gap: var(--ha-space-4);
}
@@ -71,7 +71,6 @@ export default class HaAutomationTriggerEditor extends LitElement {
`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.trigger}
.readOnly=${this.disabled}
@value-changed=${this._onYamlChange}
@@ -760,7 +760,6 @@ export default class HaAutomationTriggerRow extends LitElement {
<ha-yaml-editor
read-only
disable-fullscreen
.hass=${this.hass}
.defaultValue=${this._triggeredResult}
></ha-yaml-editor>
`,
@@ -34,7 +34,6 @@ export class HaEventTrigger extends LitElement implements TriggerElement {
@change=${this._valueChanged}
></ha-input>
<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.event.event_data"
)}
@@ -221,7 +221,6 @@ export class HaPlatformTrigger extends LitElement {
: nothing}
${shouldRenderDataYaml
? html`<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.components.service-control.action_data"
)}
@@ -42,7 +42,6 @@ class SupervisorFormfieldLabel extends LitElement {
margin-right: 4px;
margin-inline-end: 4px;
margin-inline-start: initial;
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-normal);
line-height: var(--ha-line-height-normal);
letter-spacing: 0.5px;
@@ -149,7 +149,6 @@ class DialogImportBlueprint extends LitElement {
<ha-code-editor
mode="yaml"
.value=${this._result.raw_data}
.hass=${this.hass}
read-only
dir="ltr"
></ha-code-editor>
+43 -53
View File
@@ -7,18 +7,17 @@ import { extractSearchParam } from "../../../common/url/search-params";
import "../../../components/ha-analytics";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-spinner";
import "../../../components/ha-svg-icon";
import "../../../components/ha-switch";
import { getSignedPath } from "../../../data/auth";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/item/ha-row-item";
import type { Analytics } from "../../../data/analytics";
import {
getAnalyticsDetails,
setAnalyticsPreferences,
} from "../../../data/analytics";
import { getSignedPath } from "../../../data/auth";
import { getConfigEntries } from "../../../data/config_entries";
import type { LabPreviewFeature } from "../../../data/labs";
import { subscribeLabFeature } from "../../../data/labs";
@@ -103,26 +102,24 @@ class ConfigAnalytics extends SubscribeMixin(LitElement) {
}
)}
</p>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
`ui.panel.config.analytics.preferences.snapshots.title`
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
`ui.panel.config.analytics.preferences.snapshots.description`
)}
</span>
<ha-switch
slot="end"
@change=${this._handleDeviceRowClick}
.checked=${!!this._analyticsDetails?.preferences.snapshots}
.disabled=${this._analyticsDetails === undefined}
></ha-switch>
</ha-md-list-item>
</ha-md-list>
<ha-row-item>
<span slot="headline">
${this.hass.localize(
`ui.panel.config.analytics.preferences.snapshots.title`
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
`ui.panel.config.analytics.preferences.snapshots.description`
)}
</span>
<ha-switch
slot="end"
@change=${this._handleDeviceRowClick}
.checked=${!!this._analyticsDetails?.preferences.snapshots}
.disabled=${this._analyticsDetails === undefined}
></ha-switch>
</ha-row-item>
</div>
<div class="card-actions">
<ha-button
@@ -163,29 +160,27 @@ class ConfigAnalytics extends SubscribeMixin(LitElement) {
}
)}
</p>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.data_collection.toggle_title"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.data_collection.toggle_description"
)}
</span>
${this._zwaveDataCollectionOptIn !== undefined
? html`
<ha-switch
slot="end"
@change=${this._zwaveDataCollectionToggled}
.checked=${this._zwaveDataCollectionOptIn === true}
></ha-switch>
`
: html`<ha-spinner slot="end" size="small"></ha-spinner>`}
</ha-md-list-item>
</ha-md-list>
<ha-row-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.data_collection.toggle_title"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.data_collection.toggle_description"
)}
</span>
${this._zwaveDataCollectionOptIn !== undefined
? html`
<ha-switch
slot="end"
@change=${this._zwaveDataCollectionToggled}
.checked=${this._zwaveDataCollectionOptIn === true}
></ha-switch>
`
: html`<ha-spinner slot="end" size="small"></ha-spinner>`}
</ha-row-item>
</div>
</ha-card>`
: nothing}
@@ -326,13 +321,8 @@ class ConfigAnalytics extends SubscribeMixin(LitElement) {
ha-card:not(:first-of-type) {
margin-top: 24px;
}
ha-md-list {
background: none;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
ha-row-item {
--ha-row-item-padding-inline: 0;
}
ha-card {
transition: box-shadow 0.3s ease;
@@ -193,9 +193,6 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
outline: none;
text-decoration: underline;
}
ha-md-list-item {
font-size: var(--ha-font-size-l);
}
div[slot="start"] {
position: relative;
}
@@ -194,7 +194,6 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
></ha-service-picker>
<ha-yaml-editor
id="yaml-editor"
.hass=${this.hass}
.defaultValue=${this._serviceData}
@value-changed=${this._yamlChanged}
></ha-yaml-editor>
@@ -204,7 +203,6 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
.hass=${this.hass}
.value=${this._serviceData}
.narrow=${this.narrow}
show-advanced
show-service-id
@value-changed=${this._serviceDataChanged}
class="card-content ui-mode-content"
@@ -238,7 +236,6 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
>
<div class="card-content">
<ha-yaml-editor
.hass=${this.hass}
read-only
auto-update
has-extra-actions
@@ -203,7 +203,6 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
? html`
<ha-code-editor
mode="yaml"
.hass=${this.hass}
.value=${dump(result).trimRight()}
read-only
dir="ltr"
@@ -242,7 +242,6 @@ export class HaDebugViewportEnvironmentCard extends LitElement {
<ha-code-editor
class="snapshot-editor"
mode="yaml"
.hass=${this.hass}
.value=${text}
read-only
.linewrap=${true}
@@ -73,7 +73,6 @@ class HaPanelDevEvent extends LitElement {
</div>
<div class="code-editor">
<ha-yaml-editor
.hass=${this.hass}
.value=${this._eventData}
.error=${!this._isValid}
@value-changed=${this._yamlChanged}
@@ -227,7 +227,6 @@ class EventSubscribeCard extends LitElement {
></ha-icon-button>
</div>
<ha-yaml-editor
.hass=${this.hass}
.value=${event.event}
auto-update
read-only
@@ -228,7 +228,6 @@ class HaPanelDevState extends LitElement {
)}
</p>
<ha-yaml-editor
.hass=${this.hass}
.value=${this._stateAttributes}
.error=${!this._validJSON}
@value-changed=${this._yamlChanged}
@@ -12,11 +12,18 @@ import {
import "@home-assistant/webawesome/dist/components/divider/divider";
import type { HassEntity } from "home-assistant-js-websocket";
import { consume, type ContextType } from "@lit/context";
import { css, type CSSResultGroup, html, LitElement, nothing } from "lit";
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 { computeAreaName } from "../../../../common/entity/compute_area_name";
import {
computeDeviceName,
getDuplicatedDeviceNames,
} from "../../../../common/entity/compute_device_name";
import { computeEntityEntryName } from "../../../../common/entity/compute_entity_name";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/chips/ha-assist-chip";
@@ -24,6 +31,7 @@ import "../../../../components/data-table/ha-data-table";
import type {
DataTableColumnContainer,
HaDataTable,
RowClickedEvent,
SelectionChangedEvent,
SortingDirection,
} from "../../../../components/data-table/ha-data-table";
@@ -45,9 +53,16 @@ import {
updateStatisticsIssues,
validateStatistics,
} from "../../../../data/recorder";
import {
apiContext,
internationalizationContext,
registriesContext,
statesContext,
} from "../../../../data/context";
import { getAreaTableColumn } from "../../common/data-table-columns";
import { KeyboardShortcutMixin } from "../../../../mixins/keyboard-shortcut-mixin";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type { HomeAssistantRegistries } from "../../../../types";
import { showConfirmationDialog } from "../../../lovelace/custom-card-helpers";
import { fixStatisticsIssue } from "./fix-statistics";
import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum";
@@ -77,13 +92,14 @@ type StatisticData = StatisticsMetaData & {
type DisplayedStatisticData = StatisticData & {
displayName: string;
device?: string;
device_full?: string;
area?: string;
issues_string?: string;
};
@customElement("developer-tools-statistics")
class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@state() private _data: StatisticData[] = [] as StatisticsMetaData[];
@@ -106,6 +122,22 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
@state() private _selectMode = false;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: registriesContext, subscribe: true })
private _registries!: ContextType<typeof registriesContext>;
@state()
@consume({ context: statesContext, subscribe: true })
private _states!: ContextType<typeof statesContext>;
@query("ha-data-table", true) private _dataTable!: HaDataTable;
@query("ha-input-search") private _searchInput!: HaInputSearch;
@@ -115,22 +147,55 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
}
private _displayData = memoizeOne(
(data: StatisticData[], localize: LocalizeFunc): DisplayedStatisticData[] =>
data.map((item) => ({
...item,
displayName: item.state
? computeStateName(item.state)
: item.name || item.statistic_id,
issues_string: item.issues
?.map(
(issue) =>
localize(
`ui.panel.config.developer-tools.tabs.statistics.issues.${issue.type}`,
issue.data
) || issue.type
)
.join(" "),
}))
(
data: StatisticData[],
localize: LocalizeFunc,
entities: HomeAssistantRegistries["entities"],
devices: HomeAssistantRegistries["devices"],
areas: HomeAssistantRegistries["areas"]
): DisplayedStatisticData[] => {
const duplicatedDeviceNames = getDuplicatedDeviceNames(devices);
return data.map((item) => {
const entry = entities[item.statistic_id];
const device = entry?.device_id ? devices[entry.device_id] : undefined;
const areaId = entry?.area_id || device?.area_id;
const area = areaId ? areas[areaId] : undefined;
const entityName = entry
? computeEntityEntryName(entry, devices, item.state)
: undefined;
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
const deviceFullName = deviceName
? duplicatedDeviceNames.has(deviceName) && areaName
? `${deviceName} (${areaName})`
: deviceName
: undefined;
return {
...item,
displayName:
entityName ||
deviceName ||
(item.state ? computeStateName(item.state) : undefined) ||
item.name ||
item.statistic_id,
device: deviceName,
device_full: deviceFullName,
area: areaName,
issues_string: item.issues
?.map(
(issue) =>
localize(
`ui.panel.config.developer-tools.tabs.statistics.issues.${issue.type}`,
issue.data
) || issue.type
)
.join(" "),
};
});
}
);
private _columns = memoizeOne(
@@ -146,6 +211,18 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
filterable: true,
flex: 2,
},
device: {
title: localize("ui.panel.config.entities.picker.headers.device"),
sortable: true,
template: (entry) => entry.device || "",
},
device_full: {
title: localize("ui.panel.config.entities.picker.headers.device"),
filterable: true,
groupable: true,
hidden: true,
},
area: getAreaTableColumn(localize),
statistic_id: {
title: localize(
"ui.panel.config.developer-tools.tabs.statistics.data_table.statistic_id"
@@ -187,7 +264,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
},
fix: {
title: "",
label: this.hass.localize(
label: localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.fix"
),
type: "icon",
@@ -231,21 +308,20 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
@click=${this._showStatisticsAdjustSumDialog}
></ha-icon-button>
`
: "",
: nothing,
},
})
);
protected render() {
const localize = this.hass.localize;
const columns = this._columns(this.hass.localize);
const columns = this._columns(this._i18n.localize);
const selectModeBtn = !this._selectMode
? html`<ha-assist-chip
class="has-dropdown select-mode-chip"
.active=${this._selectMode}
@click=${this._enableSelectMode}
.title=${localize(
.title=${this._i18n.localize(
"ui.components.subpage-data-table.enter_selection_mode"
)}
>
@@ -265,11 +341,14 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
<ha-dropdown @wa-select=${this._handleSortBy}>
<ha-assist-chip
slot="trigger"
.label=${localize("ui.components.subpage-data-table.sort_by", {
sortColumn: this._sortColumn
? ` ${columns[this._sortColumn]?.title || columns[this._sortColumn]?.label}`
: "",
})}
.label=${this._i18n.localize(
"ui.components.subpage-data-table.sort_by",
{
sortColumn: this._sortColumn
? ` ${columns[this._sortColumn]?.title || columns[this._sortColumn]?.label}`
: "",
}
)}
>
<ha-svg-icon
slot="trailing-icon"
@@ -307,11 +386,14 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
<ha-dropdown @wa-select=${this._handleOverflowGroupBy}>
<ha-assist-chip
slot="trigger"
.label=${localize("ui.components.subpage-data-table.group_by", {
groupColumn: this._groupColumn
? ` ${columns[this._groupColumn].title || columns[this._groupColumn].label}`
: "",
})}
.label=${this._i18n.localize(
"ui.components.subpage-data-table.group_by",
{
groupColumn: this._groupColumn
? ` ${columns[this._groupColumn].title || columns[this._groupColumn].label}`
: "",
}
)}
>
<ha-svg-icon
slot="trailing-icon"
@@ -334,7 +416,9 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
value="none"
.selected=${this._groupColumn === undefined}
>
${localize("ui.components.subpage-data-table.dont_group_by")}
${this._i18n.localize(
"ui.components.subpage-data-table.dont_group_by"
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item
@@ -346,7 +430,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
slot="icon"
.path=${mdiUnfoldLessHorizontal}
></ha-svg-icon>
${localize(
${this._i18n.localize(
"ui.components.subpage-data-table.collapse_all_groups"
)}
</ha-dropdown-item>
@@ -358,7 +442,9 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
slot="icon"
.path=${mdiUnfoldMoreHorizontal}
></ha-svg-icon>
${localize("ui.components.subpage-data-table.expand_all_groups")}
${this._i18n.localize(
"ui.components.subpage-data-table.expand_all_groups"
)}
</ha-dropdown-item>
</ha-dropdown>
`
@@ -367,7 +453,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
const settingsButton = html`<ha-assist-chip
class="has-dropdown select-mode-chip"
@click=${this._openSettings}
.title=${localize("ui.components.subpage-data-table.settings")}
.title=${this._i18n.localize("ui.components.subpage-data-table.settings")}
>
<ha-svg-icon slot="icon" .path=${mdiTableCog}></ha-svg-icon>
</ha-assist-chip>`;
@@ -380,13 +466,13 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
<ha-icon-button
.path=${mdiClose}
@click=${this._disableSelectMode}
.label=${localize(
.label=${this._i18n.localize(
"ui.components.subpage-data-table.exit_selection_mode"
)}
></ha-icon-button>
<ha-dropdown>
<ha-assist-chip
.label=${localize(
.label=${this._i18n.localize(
"ui.components.subpage-data-table.select"
)}
slot="trigger"
@@ -401,34 +487,41 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
></ha-svg-icon
></ha-assist-chip>
<ha-dropdown-item @click=${this._selectAll}>
${localize("ui.components.subpage-data-table.select_all")}
${this._i18n.localize(
"ui.components.subpage-data-table.select_all"
)}
</ha-dropdown-item>
<ha-dropdown-item @click=${this._selectAllIssues}>
${localize(
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.statistics.data_table.select_all_issues"
)}
</ha-dropdown-item>
<ha-dropdown-item @click=${this._selectNone}>
${localize("ui.components.subpage-data-table.select_none")}
${this._i18n.localize(
"ui.components.subpage-data-table.select_none"
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item @click=${this._disableSelectMode}>
${localize(
${this._i18n.localize(
"ui.components.subpage-data-table.exit_selection_mode"
)}
</ha-dropdown-item>
</ha-dropdown>
<p>
${localize("ui.components.subpage-data-table.selected", {
selected: this._selected.length,
})}
${this._i18n.localize(
"ui.components.subpage-data-table.selected",
{
selected: this._selected.length,
}
)}
</p>
</div>
<div class="center-vertical">
<slot name="selection-bar"></slot>
</div>
<ha-assist-chip
.label=${localize(
.label=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.statistics.delete_selected"
)}
.disabled=${!this._selected.length}
@@ -448,12 +541,18 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
</slot>
</div>
`
: ""}
: nothing}
<ha-data-table
.narrow=${this.narrow}
.columns=${columns}
.data=${this._displayData(this._data, this.hass.localize)}
.noDataText=${this.hass.localize(
.data=${this._displayData(
this._data,
this._i18n.localize,
this._registries.entities,
this._registries.devices,
this._registries.areas
)}
.noDataText=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.statistics.data_table.no_statistics"
)}
.filter=${this.filter}
@@ -551,7 +650,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
private _openSettings() {
showDataTableSettingsDialog(this, {
columns: this._columns(this.hass.localize),
columns: this._columns(this._i18n.localize),
hiddenColumns: this.hiddenColumns,
columnOrder: this.columnOrder,
onUpdate: (
@@ -561,7 +660,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
this.columnOrder = columnOrder;
this.hiddenColumns = hiddenColumns;
},
localizeFunc: this.hass.localize,
localizeFunc: this._i18n.localize,
});
}
@@ -599,27 +698,29 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
);
}
private _showStatisticsAdjustSumDialog(ev) {
private _showStatisticsAdjustSumDialog(ev: Event) {
ev.stopPropagation();
showStatisticsAdjustSumDialog(this, {
statistic: ev.currentTarget.statistic,
statistic: (
ev.currentTarget as HTMLElement & { statistic: StatisticData }
).statistic,
});
}
private _rowClicked(ev) {
private _rowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const id = ev.detail.id;
if (id in this.hass.states) {
if (id in this._states) {
fireEvent(this, "hass-more-info", { entityId: id });
}
}
private async _validateStatistics() {
const [statisticIds, issues] = await Promise.all([
getStatisticIds(this.hass),
validateStatistics(this.hass),
getStatisticIds(this._api),
validateStatistics(this._api),
]);
updateStatisticsIssues(this.hass);
updateStatisticsIssues(this._api);
const statsIds = new Set();
@@ -627,7 +728,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
statsIds.add(statistic.statistic_id);
return {
...statistic,
state: this.hass.states[statistic.statistic_id],
state: this._states[statistic.statistic_id],
issues: issues[statistic.statistic_id],
};
});
@@ -638,7 +739,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
statistic_id: statisticId,
statistics_unit_of_measurement: "",
source: "",
state: this.hass.states[statisticId],
state: this._states[statisticId],
issues: issues[statisticId],
mean_type: StatisticMeanType.NONE,
has_sum: false,
@@ -656,25 +757,27 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
const deletableIds = this._selected;
await showConfirmationDialog(this, {
title: this.hass.localize(
title: this._i18n.localize(
"ui.panel.config.developer-tools.tabs.statistics.multi_delete.title"
),
text: html`${this.hass.localize(
text: html`${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.statistics.multi_delete.info_text",
{ statistic_count: deletableIds.length }
)}`,
confirmText: this.hass.localize("ui.common.delete"),
confirmText: this._i18n.localize("ui.common.delete"),
destructive: true,
confirm: async () => {
await clearStatistics(this.hass, deletableIds);
await clearStatistics(this._api, deletableIds);
this._validateStatistics();
this._dataTable.clearSelection();
},
});
};
private _fixIssue = async (ev) => {
const issues = (ev.currentTarget.data as StatisticsValidationResult[]).sort(
private _fixIssue = async (ev: Event) => {
const issues = (
ev.currentTarget as HTMLElement & { data: StatisticsValidationResult[] }
).data.sort(
(itemA, itemB) =>
(FIX_ISSUES_ORDER[itemA.type] ?? 99) -
(FIX_ISSUES_ORDER[itemB.type] ?? 99)
@@ -1,7 +1,7 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { debounce } from "../../../../common/util/debounce";
@@ -58,10 +58,14 @@ class HaPanelDevTemplate extends LitElement {
@state() private _descriptionExpanded = false;
@query("ha-tip") private _editorTip?: HTMLElement;
private _template = "";
private _inited = false;
private _tipResizeObserver?: ResizeObserver;
public connectedCallback() {
super.connectedCallback();
if (this._template && !this._unsubRenderTemplate) {
@@ -72,6 +76,8 @@ class HaPanelDevTemplate extends LitElement {
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubscribeTemplate();
this._tipResizeObserver?.disconnect();
this._tipResizeObserver = undefined;
}
protected firstUpdated() {
@@ -81,6 +87,7 @@ class HaPanelDevTemplate extends LitElement {
this._template = DEMO_TEMPLATE;
}
this._subscribeTemplate();
this._observeTipHeight();
this._inited = true;
}
@@ -154,7 +161,6 @@ class HaPanelDevTemplate extends LitElement {
<div class="card-content">
<ha-code-editor
mode="jinja2"
.hass=${this.hass}
.value=${this._template}
.error=${this._error}
autofocus
@@ -289,6 +295,21 @@ ${type === "object"
`;
}
private _observeTipHeight() {
if (!this._editorTip || this._tipResizeObserver) {
return;
}
this._tipResizeObserver = new ResizeObserver((entries) => {
const height =
entries[0]?.borderBoxSize?.[0]?.blockSize ??
entries[0]?.contentRect.height;
if (height) {
this.style.setProperty("--tip-height", `${height}px`);
}
});
this._tipResizeObserver.observe(this._editorTip);
}
private _expandedChanged(
ev: HASSDomEvent<HASSDomEvents["expanded-changed"]>
) {
@@ -332,6 +353,9 @@ ${type === "object"
var(--ha-card-header-font-size, var(--ha-font-size-2xl))
);
--card-actions-height: calc(1px + var(--ha-space-2) * 2 + 40px);
--tip-height-minimal: calc(
var(--mdc-icon-size, 24px) + var(--ha-space-4)
);
--edit-pane-height: calc(
100vh - var(--panel-header-height) - var(
--description-pane-height
@@ -341,8 +365,9 @@ ${type === "object"
--code-mirror-max-height: calc(
var(--edit-pane-height) - var(--card-header-height) +
var(--ha-space-2) - var(--card-actions-height) - var(
--ha-space-4
) - var(--ha-card-border-width, 1px) *
--tip-height,
var(--tip-height-minimal)
) - var(--ha-space-4) - var(--ha-card-border-width, 1px) *
2
);
}
@@ -96,7 +96,8 @@ export class EnergyGasSettings extends LitElement {
></ha-icon>`
: html`<ha-svg-icon .path=${mdiFire}></ha-svg-icon>`}
<span class="content"
>${getStatisticLabel(
>${source.name ||
getStatisticLabel(
this.hass,
source.stat_energy_from,
this.statsMetadata?.[source.stat_energy_from]
@@ -141,6 +142,7 @@ export class EnergyGasSettings extends LitElement {
private _addSource() {
showEnergySettingsGasDialog(this, {
statsMetadata: this.statsMetadata,
allowedGasUnitClass: getEnergyGasUnitClass(
this.preferences,
undefined,
@@ -164,12 +166,12 @@ export class EnergyGasSettings extends LitElement {
ev.currentTarget.closest(".row").source;
showEnergySettingsGasDialog(this, {
source: { ...origSource },
statsMetadata: this.statsMetadata,
allowedGasUnitClass: getEnergyGasUnitClass(
this.preferences,
origSource.stat_energy_from,
this.statsMetadata
),
metadata: this.statsMetadata?.[origSource.stat_energy_from],
gas_sources: this.preferences.energy_sources.filter(
(src) => src.type === "gas"
) as GasSourceTypeEnergyPreference[],
@@ -95,7 +95,8 @@ export class EnergyWaterSettings extends LitElement {
></ha-icon>`
: html`<ha-svg-icon .path=${mdiWater}></ha-svg-icon>`}
<span class="content"
>${getStatisticLabel(
>${source.name ||
getStatisticLabel(
this.hass,
source.stat_energy_from,
this.statsMetadata?.[source.stat_energy_from]
@@ -140,6 +141,7 @@ export class EnergyWaterSettings extends LitElement {
private _addSource() {
showEnergySettingsWaterDialog(this, {
statsMetadata: this.statsMetadata,
water_sources: this.preferences.energy_sources.filter(
(src) => src.type === "water"
) as WaterSourceTypeEnergyPreference[],
@@ -157,8 +159,8 @@ export class EnergyWaterSettings extends LitElement {
const origSource: WaterSourceTypeEnergyPreference =
ev.currentTarget.closest(".row").source;
showEnergySettingsWaterDialog(this, {
statsMetadata: this.statsMetadata,
source: { ...origSource },
metadata: this.statsMetadata?.[origSource.stat_energy_from],
water_sources: this.preferences.energy_sources.filter(
(src) => src.type === "water"
) as WaterSourceTypeEnergyPreference[],
@@ -13,7 +13,11 @@ import "../../../../components/input/ha-input";
import type { HaInput } from "../../../../components/input/ha-input";
import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy";
import { energyStatisticHelpUrl } from "../../../../data/energy";
import { getStatisticLabel } from "../../../../data/recorder";
import {
getStatisticLabel,
getStatisticMetadata,
isExternalStatistic,
} from "../../../../data/recorder";
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../resources/styles";
@@ -232,13 +236,27 @@ export class DialogEnergyDeviceSettingsWater
`;
}
private _statisticChanged(ev: ValueChangedEvent<string>) {
private async _statisticChanged(ev: ValueChangedEvent<string>) {
if (!ev.detail.value) {
this._device = undefined;
return;
}
this._device = { stat_consumption: ev.detail.value };
this._computePossibleParents();
if (
isExternalStatistic(ev.detail.value) &&
this._params?.statsMetadata &&
!(ev.detail.value in this._params.statsMetadata)
) {
const [metadata] = await getStatisticMetadata(this.hass, [
ev.detail.value,
]);
if (metadata) {
this._params.statsMetadata[ev.detail.value] = metadata;
this.requestUpdate("_params");
}
}
}
private _flowRateStatisticChanged(ev: ValueChangedEvent<string>) {
@@ -16,7 +16,11 @@ import "../../../../components/input/ha-input";
import type { HaInput } from "../../../../components/input/ha-input";
import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy";
import { energyStatisticHelpUrl } from "../../../../data/energy";
import { getStatisticLabel } from "../../../../data/recorder";
import {
getStatisticLabel,
getStatisticMetadata,
isExternalStatistic,
} from "../../../../data/recorder";
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../resources/styles";
@@ -232,13 +236,27 @@ export class DialogEnergyDeviceSettings
`;
}
private _statisticChanged(ev: ValueChangedEvent<string>) {
private async _statisticChanged(ev: ValueChangedEvent<string>) {
if (!ev.detail.value) {
this._device = undefined;
return;
}
this._device = { stat_consumption: ev.detail.value };
this._computePossibleParents();
if (
isExternalStatistic(ev.detail.value) &&
this._params?.statsMetadata &&
!(ev.detail.value in this._params.statsMetadata)
) {
const [metadata] = await getStatisticMetadata(this.hass, [
ev.detail.value,
]);
if (metadata) {
this._params.statsMetadata[ev.detail.value] = metadata;
this.requestUpdate("_params");
}
}
}
private _powerStatisticChanged(ev: ValueChangedEvent<string>) {
@@ -19,6 +19,7 @@ import {
} from "../../../../data/energy";
import {
getDisplayUnit,
getStatisticLabel,
getStatisticMetadata,
isExternalStatistic,
} from "../../../../data/recorder";
@@ -27,6 +28,7 @@ import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
import type { EnergySettingsGasDialogParams } from "./show-dialogs-energy";
import type { HaInput } from "../../../../components/input/ha-input";
const gasDeviceClasses = ["gas", "energy"];
const gasUnitClasses = ["volume", "energy"];
@@ -71,7 +73,9 @@ export class DialogEnergyGasSettings
this._pickedDisplayUnit = getDisplayUnit(
this.hass,
params.source?.stat_energy_from,
params.metadata
params.source?.stat_energy_from
? params.statsMetadata?.[params.source?.stat_energy_from]
: undefined
);
this._costs = this._source.entity_energy_price
? "entity"
@@ -196,6 +200,24 @@ export class DialogEnergyGasSettings
)}
></ha-statistic-picker>
<ha-input
.label=${this.hass.localize(
"ui.panel.config.energy.gas.dialog.display_name"
)}
type="text"
.disabled=${!this._source?.stat_energy_from}
.value=${this._source?.name || ""}
.placeholder=${this._source?.stat_energy_from
? getStatisticLabel(
this.hass,
this._source.stat_energy_from,
this._params?.statsMetadata?.[this._source.stat_energy_from]
)
: ""}
@input=${this._nameChanged}
>
</ha-input>
<ha-radio-group
.label=${this.hass.localize(
"ui.panel.config.energy.gas.dialog.cost_para"
@@ -350,11 +372,22 @@ export class DialogEnergyGasSettings
private async _statisticChanged(ev: ValueChangedEvent<string>) {
if (ev.detail.value) {
const metadata = await getStatisticMetadata(this.hass, [ev.detail.value]);
const [metadata] = await getStatisticMetadata(this.hass, [
ev.detail.value,
]);
if (
metadata &&
isExternalStatistic(ev.detail.value) &&
this._params?.statsMetadata &&
!(ev.detail.value in this._params.statsMetadata)
) {
this._params.statsMetadata[ev.detail.value] = metadata;
this.requestUpdate("_params");
}
this._pickedDisplayUnit = getDisplayUnit(
this.hass,
ev.detail.value,
metadata[0]
metadata
);
if (isExternalStatistic(ev.detail.value) && this._costs !== "statistic") {
this._costs = "no-costs";
@@ -368,6 +401,16 @@ export class DialogEnergyGasSettings
};
}
private _nameChanged(ev: InputEvent) {
this._source = {
...this._source!,
name: (ev.target as HaInput).value,
};
if (!this._source.name) {
delete this._source.name;
}
}
private async _save() {
try {
if (this._costs === "no-costs") {
@@ -17,12 +17,17 @@ import {
emptyWaterEnergyPreference,
energyStatisticHelpUrl,
} from "../../../../data/energy";
import { isExternalStatistic } from "../../../../data/recorder";
import {
getStatisticLabel,
getStatisticMetadata,
isExternalStatistic,
} from "../../../../data/recorder";
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
import type { EnergySettingsWaterDialogParams } from "./show-dialogs-energy";
import type { HaInput } from "../../../../components/input/ha-input";
const flowRateUnitClasses = ["volume_flow_rate"];
@@ -154,6 +159,24 @@ export class DialogEnergyWaterSettings
)}
></ha-statistic-picker>
<ha-input
.label=${this.hass.localize(
"ui.panel.config.energy.water.dialog.display_name"
)}
type="text"
.disabled=${!this._source?.stat_energy_from}
.value=${this._source?.name || ""}
.placeholder=${this._source?.stat_energy_from
? getStatisticLabel(
this.hass,
this._source.stat_energy_from,
this._params?.statsMetadata?.[this._source.stat_energy_from]
)
: ""}
@input=${this._nameChanged}
>
</ha-input>
<ha-radio-group
.label=${this.hass.localize(
"ui.panel.config.energy.water.dialog.cost_para"
@@ -300,6 +323,31 @@ export class DialogEnergyWaterSettings
...this._source!,
stat_energy_from: ev.detail.value,
};
if (
ev.detail.value &&
isExternalStatistic(ev.detail.value) &&
this._params?.statsMetadata &&
!(ev.detail.value in this._params.statsMetadata)
) {
const [metadata] = await getStatisticMetadata(this.hass, [
ev.detail.value,
]);
if (metadata) {
this._params.statsMetadata[ev.detail.value] = metadata;
this.requestUpdate("_params");
}
}
}
private _nameChanged(ev: InputEvent) {
this._source = {
...this._source!,
name: (ev.target as HaInput).value,
};
if (!this._source.name) {
delete this._source.name;
}
}
private async _save() {
@@ -33,15 +33,15 @@ export interface EnergySettingsBatteryDialogParams {
export interface EnergySettingsGasDialogParams {
source?: GasSourceTypeEnergyPreference;
allowedGasUnitClass?: EnergyGasUnitClass;
metadata?: StatisticsMetaData;
gas_sources: GasSourceTypeEnergyPreference[];
statsMetadata?: Record<string, StatisticsMetaData>;
saveCallback: (source: GasSourceTypeEnergyPreference) => Promise<void>;
}
export interface EnergySettingsWaterDialogParams {
source?: WaterSourceTypeEnergyPreference;
metadata?: StatisticsMetaData;
water_sources: WaterSourceTypeEnergyPreference[];
statsMetadata?: Record<string, StatisticsMetaData>;
saveCallback: (source: WaterSourceTypeEnergyPreference) => Promise<void>;
}
@@ -135,7 +135,6 @@ class HaConfigHardwareAll extends LitElement {
return html`<ha-code-editor
mode="yaml"
.hass=${this.hass}
.value=${dump(device.attributes, { indent: 2 })}
read-only
></ha-code-editor>`;
@@ -2,8 +2,6 @@ import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import "../../../../../../components/ha-icon-next";
import "../../../../../../components/ha-md-list-item";
import "../../../../../../components/ha-md-list";
import "../../../../../../components/input/ha-input";
import type { HomeAssistant } from "../../../../../../types";
import { sharedStyles } from "./matter-add-device-shared-styles";
@@ -2,8 +2,6 @@ import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import "../../../../../../components/ha-icon-next";
import "../../../../../../components/ha-md-list-item";
import "../../../../../../components/ha-md-list";
import "../../../../../../components/input/ha-input";
import type { HomeAssistant } from "../../../../../../types";
import { sharedStyles } from "./matter-add-device-shared-styles";
@@ -2,8 +2,6 @@ import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import "../../../../../../components/ha-icon-next";
import "../../../../../../components/ha-md-list-item";
import "../../../../../../components/ha-md-list";
import "../../../../../../components/input/ha-input";
import type { HomeAssistant } from "../../../../../../types";
import { sharedStyles } from "./matter-add-device-shared-styles";
@@ -2,8 +2,6 @@ import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import "../../../../../../components/ha-icon-next";
import "../../../../../../components/ha-md-list-item";
import "../../../../../../components/ha-md-list";
import type { HomeAssistant } from "../../../../../../types";
import { sharedStyles } from "./matter-add-device-shared-styles";
@@ -107,7 +107,6 @@ export class MQTTConfigPanel extends LitElement {
mode="jinja2"
autocomplete-entities
autocomplete-icons
.hass=${this.hass}
.value=${this._payload}
@value-changed=${this._handlePayload}
dir="ltr"
@@ -1,11 +1,13 @@
import { LitElement, html, nothing, css } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, state } from "lit/decorators";
import type { TemplateResult } from "lit";
import type { ContextType } from "@lit/context";
import { consume } from "@lit/context";
import { dump } from "js-yaml";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-code-editor";
import "../../../../../components/ha-dialog";
import { internationalizationContext } from "../../../../../data/context";
export interface SSDPRawDataDialogParams {
key: string;
@@ -14,12 +16,14 @@ export interface SSDPRawDataDialogParams {
@customElement("dialog-ssdp-raw-data")
class DialogSSDPRawData extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: SSDPRawDataDialogParams;
@state() private _open = false;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
public async showDialog(params: SSDPRawDataDialogParams): Promise<void> {
this._params = params;
this._open = true;
@@ -42,13 +46,12 @@ class DialogSSDPRawData extends LitElement {
return html`
<ha-dialog
.open=${this._open}
header-title=${`${this.hass.localize("ui.panel.config.ssdp.raw_data_title")}: ${this._params.key}`}
header-title=${`${this._i18n.localize("ui.panel.config.ssdp.raw_data_title")}: ${this._params.key}`}
@closed=${this._dialogClosed}
>
<ha-code-editor
mode="yaml"
.value=${dump(this._params.data)}
.hass=${this.hass}
read-only
autofocus
></ha-code-editor>
@@ -35,13 +35,7 @@ class ZHADeviceZigbeeInfo extends LitElement {
}
return html`
<ha-code-editor
mode="yaml"
read-only
.hass=${this.hass}
.value=${this._signature}
dir="ltr"
>
<ha-code-editor mode="yaml" read-only .value=${this._signature} dir="ltr">
</ha-code-editor>
`;
}
@@ -89,9 +89,6 @@ export class ZWaveJsAddNodeSelectMethod extends LitElement {
display: block;
padding: 16px;
}
ha-md-list {
padding: 0;
}
`;
}
@@ -6,13 +6,13 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/ha-entities-picker";
import "../../../components/ha-button";
import "../../../components/ha-dialog";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-icon-button";
import "../../../components/ha-picture-upload";
import type { HaPictureUpload } from "../../../components/ha-picture-upload";
import "../../../components/ha-md-list-item";
import "../../../components/input/ha-input";
import "../../../components/ha-dialog";
import "../../../components/item/ha-row-item";
import { adminChangeUsername } from "../../../data/auth";
import type { PersonMutableParams } from "../../../data/person";
import type { User } from "../../../data/user";
@@ -163,7 +163,7 @@ class DialogPersonDetail extends LitElement implements HassDialog {
@change=${this._pictureChanged}
></ha-picture-upload>
<ha-md-list-item>
<ha-row-item>
<span slot="headline"
>${this.hass!.localize(
"ui.panel.config.person.detail.allow_login"
@@ -183,7 +183,7 @@ class DialogPersonDetail extends LitElement implements HassDialog {
this._user.is_owner)}
.checked=${this._userId}
></ha-switch>
</ha-md-list-item>
</ha-row-item>
${this._renderUserFields()}
${this._deviceTrackersAvailable(this.hass)
@@ -279,7 +279,7 @@ class DialogPersonDetail extends LitElement implements HassDialog {
return html`
${!user.system_generated
? html`
<ha-md-list-item>
<ha-row-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.person.detail.username"
@@ -299,12 +299,12 @@ class DialogPersonDetail extends LitElement implements HassDialog {
</ha-icon-button>
`
: nothing}
</ha-md-list-item>
</ha-row-item>
`
: nothing}
${!user.system_generated && this.hass.user?.is_owner
? html`
<ha-md-list-item>
<ha-row-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.person.detail.password"
@@ -324,10 +324,10 @@ class DialogPersonDetail extends LitElement implements HassDialog {
</ha-icon-button>
`
: nothing}
</ha-md-list-item>
</ha-row-item>
`
: nothing}
<ha-md-list-item>
<ha-row-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.person.detail.local_access_only"
@@ -344,8 +344,8 @@ class DialogPersonDetail extends LitElement implements HassDialog {
.checked=${this._localOnly}
@change=${this._localOnlyChanged}
></ha-switch>
</ha-md-list-item>
<ha-md-list-item>
</ha-row-item>
<ha-row-item>
<span slot="headline"
>${this.hass.localize("ui.panel.config.person.detail.admin")}</span
>
@@ -360,7 +360,7 @@ class DialogPersonDetail extends LitElement implements HassDialog {
.checked=${this._isAdmin}
@change=${this._adminChanged}
></ha-switch>
</ha-md-list-item>
</ha-row-item>
`;
}
@@ -560,10 +560,8 @@ class DialogPersonDetail extends LitElement implements HassDialog {
margin-bottom: 16px;
--file-upload-image-border-radius: var(--ha-border-radius-circle);
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-item-overflow: visible;
ha-row-item {
--ha-row-item-padding-inline: 0;
}
a {
color: var(--primary-color);
@@ -325,7 +325,6 @@ export class HaSceneEditor extends PreventUnsavedMixin(
private _renderYamlMode() {
return html` <ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._config}
@value-changed=${this._yamlChanged}
@editor-save=${this._saveScene}
@@ -458,7 +458,6 @@ export class HaScriptEditor extends SubscribeMixin(
`
: this.mode === "yaml"
? html`<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._preprocessYaml()}
.readOnly=${this.readOnly}
disable-fullscreen
@@ -69,7 +69,6 @@ export default class HaScriptFieldEditor extends LitElement {
</ha-alert>`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${yamlValue}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>`
@@ -87,7 +87,6 @@ export default class HaScriptFieldSelectorEditor extends LitElement {
</ha-alert>`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${data}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>`
@@ -267,7 +267,6 @@ export class HaScriptTrace extends LitElement {
: this._view === "config"
? html`
<ha-trace-config
.hass=${this.hass}
.trace=${this._trace}
></ha-trace-config>
`
@@ -283,7 +282,6 @@ export class HaScriptTrace extends LitElement {
: this._view === "blueprint"
? html`
<ha-trace-blueprint-config
.hass=${this.hass}
.trace=${this._trace}
></ha-trace-blueprint-config>
`
+7 -9
View File
@@ -7,11 +7,11 @@ import "../../../components/ha-button";
import "../../../components/ha-dialog";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-list-item";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/input/ha-input";
import type { HaInput } from "../../../components/input/ha-input";
import "../../../components/item/ha-row-item";
import { createAuthForUser } from "../../../data/auth";
import type { User } from "../../../data/user";
import {
@@ -157,7 +157,7 @@ export class DialogAddUser extends LitElement {
"ui.panel.config.users.add_user.password_not_match"
)}
></ha-input>
<ha-md-list-item>
<ha-row-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.users.editor.local_access_only"
@@ -173,8 +173,8 @@ export class DialogAddUser extends LitElement {
.checked=${this._localOnly}
@change=${this._localOnlyChanged}
></ha-switch>
</ha-md-list-item>
<ha-md-list-item>
</ha-row-item>
<ha-row-item>
<span slot="headline"
>${this.hass.localize("ui.panel.config.users.editor.admin")}</span
>
@@ -188,7 +188,7 @@ export class DialogAddUser extends LitElement {
.checked=${this._isAdmin}
@change=${this._adminChanged}
></ha-switch>
</ha-md-list-item>
</ha-row-item>
${!this._isAdmin
? html`
<ha-alert alert-type="info">
@@ -320,10 +320,8 @@ export class DialogAddUser extends LitElement {
display: flex;
padding: 8px 0;
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-item-overflow: visible;
ha-row-item {
--ha-row-item-padding-inline: 0;
}
`,
];
+13 -15
View File
@@ -9,11 +9,11 @@ import "../../../components/ha-dialog";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-icon-button";
import "../../../components/ha-label";
import "../../../components/ha-md-list-item";
import "../../../components/ha-svg-icon";
import "../../../components/ha-switch";
import "../../../components/input/ha-input";
import type { HaInput } from "../../../components/input/ha-input";
import "../../../components/item/ha-row-item";
import { adminChangeUsername } from "../../../data/auth";
import {
computeUserBadges,
@@ -111,7 +111,7 @@ class DialogUserDetail extends LitElement {
"ui.panel.config.users.editor.name"
)}
></ha-input>
<ha-md-list-item>
<ha-row-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.users.editor.username"
@@ -131,14 +131,14 @@ class DialogUserDetail extends LitElement {
</ha-icon-button>
`
: nothing}
</ha-md-list-item>
</ha-row-item>
`
: nothing
}
${
!user.system_generated && this.hass.user?.is_owner
? html`
<ha-md-list-item>
<ha-row-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.users.editor.password"
@@ -158,11 +158,11 @@ class DialogUserDetail extends LitElement {
</ha-icon-button>
`
: nothing}
</ha-md-list-item>
</ha-row-item>
`
: nothing
}
<ha-md-list-item>
<ha-row-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.users.editor.active"
@@ -179,8 +179,8 @@ class DialogUserDetail extends LitElement {
.checked=${this._isActive}
@change=${this._activeChanged}
></ha-switch>
</ha-md-list-item>
<ha-md-list-item>
</ha-row-item>
<ha-row-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.users.editor.local_access_only"
@@ -197,8 +197,8 @@ class DialogUserDetail extends LitElement {
.checked=${this._localOnly}
@change=${this._localOnlyChanged}
></ha-switch>
</ha-md-list-item>
<ha-md-list-item>
</ha-row-item>
<ha-row-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.users.editor.admin"
@@ -216,7 +216,7 @@ class DialogUserDetail extends LitElement {
@change=${this._adminChanged}
></ha-switch>
</ha-switch>
</ha-md-list-item>
</ha-row-item>
${
!this._isAdmin && !user.system_generated
? html`
@@ -398,10 +398,8 @@ class DialogUserDetail extends LitElement {
.secondary {
color: var(--secondary-text-color);
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-item-overflow: visible;
ha-row-item {
--ha-row-item-padding-inline: 0;
}
.badge-container {
margin-top: 4px;
@@ -129,12 +129,7 @@ const dataMinusKeysRender = (
<span slot="header"
>${hass.localize("ui.panel.config.voice_assistants.debug.raw")}</span
>
<ha-yaml-editor
readOnly
autoUpdate
.hass=${hass}
.value=${result}
></ha-yaml-editor>
<ha-yaml-editor readOnly autoUpdate .value=${result}></ha-yaml-editor>
</ha-expansion-panel>`
: "";
};
@@ -242,7 +237,6 @@ export class AssistPipelineDebug extends LitElement {
<ha-yaml-editor
read-only
auto-update
.hass=${this.hass}
.value=${content}
></ha-yaml-editor>
</ha-expansion-panel>
@@ -271,7 +265,6 @@ export class AssistPipelineDebug extends LitElement {
<ha-yaml-editor
read-only
auto-update
.hass=${this.hass}
.value=${content.tool_calls}
></ha-yaml-editor>
</ha-expansion-panel>
@@ -535,7 +528,6 @@ export class AssistPipelineDebug extends LitElement {
<ha-yaml-editor
read-only
auto-update
.hass=${this.hass}
.value=${this.pipelineRun}
></ha-yaml-editor>
</ha-expansion-panel>
+1 -1
View File
@@ -392,7 +392,7 @@ class PanelHome extends LitElement {
gap: var(--ha-space-2);
position: fixed;
top: var(--header-height, 56px);
left: var(--mdc-drawer-width, 0px);
left: var(--ha-sidebar-width, 0px);
right: 0;
z-index: 5;
}
@@ -8,7 +8,9 @@ import {
mdiSkipNext,
mdiSkipPrevious,
mdiStop,
mdiVolumeHigh,
mdiVolumeMinus,
mdiVolumeOff,
mdiVolumePlus,
} from "@mdi/js";
import type { PropertyValues } from "lit";
@@ -56,6 +58,7 @@ const MEDIA_PLAYER_PLAYBACK_CONTROLS_FEATURES: Record<
media_next_track: [MediaPlayerEntityFeature.NEXT_TRACK],
volume_down: [MediaPlayerEntityFeature.VOLUME_STEP],
volume_up: [MediaPlayerEntityFeature.VOLUME_STEP],
volume_mute: [MediaPlayerEntityFeature.VOLUME_MUTE],
};
export const supportsMediaPlayerPlaybackControl = (
@@ -280,6 +283,16 @@ class HuiMediaPlayerPlaybackCardFeature
buttons.push({ icon: mdiVolumePlus, action: "volume_up" });
}
break;
case "volume_mute":
if (supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_MUTE)) {
buttons.push({
icon: stateObj.attributes.is_volume_muted
? mdiVolumeOff
: mdiVolumeHigh,
action: "volume_mute",
});
}
break;
}
}
@@ -315,6 +328,14 @@ class HuiMediaPlayerPlaybackCardFeature
return;
}
if (action === "volume_mute") {
this.hass!.callService("media_player", "volume_mute", {
entity_id: this._stateObj.entity_id,
is_volume_muted: !this._stateObj.attributes.is_volume_muted,
});
return;
}
this.hass!.callService("media_player", action, {
entity_id: this._stateObj.entity_id,
});
@@ -8,7 +8,7 @@ import {
} from "../../../data/media-player";
import type { HomeAssistant } from "../../../types";
import { hasConfigChanged } from "../common/has-changed";
import type { LovelaceCardFeature } from "../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { HuiModeSelectCardFeatureBase } from "./hui-mode-select-card-feature-base";
import type {
LovelaceCardFeatureContext,
@@ -43,6 +43,11 @@ class HuiMediaPlayerSourceCardFeature
protected readonly _modesAttribute = "source_list";
protected get _configuredModes() {
const sources = this._config?.sources;
return sources?.length ? sources : undefined;
}
protected readonly _serviceDomain = "media_player";
protected readonly _serviceAction = "select_source";
@@ -63,6 +68,13 @@ class HuiMediaPlayerSourceCardFeature
};
}
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
await import("../editor/config-elements/hui-media-player-source-card-feature-editor");
return document.createElement(
"hui-media-player-source-card-feature-editor"
);
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
const entityId = this.context?.entity_id;
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;

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