Compare commits

..

6 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
f8d8bbf5f9 Remove underscore prefix from protected members per style guide
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2026-02-27 09:41:44 +00:00
copilot-swe-agent[bot]
be810d1f09 Add shared styles and loading animation to automation/script editor mixin
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2026-02-26 12:45:35 +00:00
Wendelin
8e6f693c55 Simplify automation/script editor mixin signature 2026-02-26 11:15:09 +01:00
copilot-swe-agent[bot]
c9d35c0500 Fix mixin: use function-body syntax for decorators, curried generics for type safety
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2026-02-26 08:17:45 +00:00
copilot-swe-agent[bot]
3b2a1ed5be Changes before error encountered
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2026-02-26 07:43:55 +00:00
copilot-swe-agent[bot]
1c50432dd4 Initial plan 2026-02-26 07:12:09 +00:00
56 changed files with 1292 additions and 1593 deletions

View File

@@ -515,14 +515,6 @@ export class DemoHaAdaptiveDialog extends LitElement {
<td><code>--ha-dialog-surface-background</code></td>
<td>Dialog/sheet background color.</td>
</tr>
<tr>
<td><code>--ha-dialog-surface-backdrop-filter</code></td>
<td>Dialog/sheet surface backdrop filter.</td>
</tr>
<tr>
<td><code>--dialog-box-shadow</code></td>
<td>Dialog surface box shadow (dialog mode only).</td>
</tr>
<tr>
<td><code>--ha-dialog-border-radius</code></td>
<td>Border radius of the dialog surface (dialog mode only).</td>
@@ -535,34 +527,6 @@ export class DemoHaAdaptiveDialog extends LitElement {
<td><code>--ha-dialog-hide-duration</code></td>
<td>Hide animation duration (dialog mode only).</td>
</tr>
<tr>
<td><code>--ha-dialog-scrim-backdrop-filter</code></td>
<td>Dialog/sheet scrim backdrop filter.</td>
</tr>
<tr>
<td><code>--dialog-backdrop-filter</code></td>
<td>Dialog/sheet scrim backdrop filter (legacy fallback).</td>
</tr>
<tr>
<td><code>--mdc-dialog-scrim-color</code></td>
<td>Dialog/sheet scrim color (legacy compatibility).</td>
</tr>
<tr>
<td><code>--ha-bottom-sheet-surface-background</code></td>
<td>Bottom sheet background color (sheet mode only).</td>
</tr>
<tr>
<td><code>--ha-bottom-sheet-surface-backdrop-filter</code></td>
<td>Bottom sheet surface backdrop filter (sheet mode only).</td>
</tr>
<tr>
<td><code>--ha-bottom-sheet-scrim-backdrop-filter</code></td>
<td>Bottom sheet scrim backdrop filter (sheet mode only).</td>
</tr>
<tr>
<td><code>--ha-bottom-sheet-scrim-color</code></td>
<td>Bottom sheet scrim color (sheet mode only).</td>
</tr>
</tbody>
</table>

View File

@@ -380,29 +380,13 @@ export class DemoHaDialog extends LitElement {
<td><code>--ha-dialog-surface-background</code></td>
<td>Dialog background color.</td>
</tr>
<tr>
<td><code>--ha-dialog-surface-backdrop-filter</code></td>
<td>Backdrop filter applied to the dialog surface.</td>
</tr>
<tr>
<td><code>--dialog-box-shadow</code></td>
<td>Dialog surface box shadow.</td>
</tr>
<tr>
<td><code>--ha-dialog-border-radius</code></td>
<td>Border radius of the dialog surface.</td>
</tr>
<tr>
<td><code>--ha-dialog-scrim-backdrop-filter</code></td>
<td>Backdrop filter applied to the dialog scrim.</td>
</tr>
<tr>
<td><code>--dialog-backdrop-filter</code></td>
<td>Legacy fallback for the dialog scrim backdrop filter.</td>
</tr>
<tr>
<td><code>--mdc-dialog-scrim-color</code></td>
<td>Dialog scrim color (legacy compatibility).</td>
<td><code>--dialog-z-index</code></td>
<td>Z-index for the dialog.</td>
</tr>
<tr>
<td><code>--dialog-surface-margin-top</code></td>

View File

@@ -92,7 +92,7 @@
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"barcode-detector": "3.1.0",
"barcode-detector": "3.0.8",
"color-name": "2.1.0",
"comlink": "4.4.2",
"core-js": "3.48.0",
@@ -149,7 +149,7 @@
"@babel/plugin-transform-runtime": "7.29.0",
"@babel/preset-env": "7.29.0",
"@bundle-stats/plugin-webpack-filter": "4.21.10",
"@html-eslint/eslint-plugin": "0.57.0",
"@html-eslint/eslint-plugin": "0.56.0",
"@lokalise/node-api": "15.6.1",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.1.0",
@@ -214,7 +214,7 @@
"terser-webpack-plugin": "5.3.16",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
"typescript-eslint": "8.56.1",
"typescript-eslint": "8.56.0",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.0.18",
"webpack-stats-plugin": "1.1.3",

View File

@@ -133,34 +133,33 @@ const computeStateToPartsFromEntityAttributes = (
),
});
} catch (_err) {
// fallback to default numeric formatting below
// fallback to default
}
if (parts.length) {
const TYPE_MAP: Record<string, ValuePart["type"]> = {
integer: "value",
group: "value",
decimal: "value",
fraction: "value",
literal: "literal",
currency: "unit",
};
const TYPE_MAP: Record<string, ValuePart["type"]> = {
integer: "value",
group: "value",
decimal: "value",
fraction: "value",
literal: "literal",
currency: "unit",
};
const valueParts: ValuePart[] = [];
const valueParts: ValuePart[] = [];
for (const part of parts) {
const type = TYPE_MAP[part.type];
if (!type) continue;
const last = valueParts[valueParts.length - 1];
// Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56")
if (type === "value" && last?.type === "value") {
last.value += part.value;
} else {
valueParts.push({ type, value: part.value });
}
for (const part of parts) {
const type = TYPE_MAP[part.type];
if (!type) continue;
const last = valueParts[valueParts.length - 1];
// Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56")
if (type === "value" && last?.type === "value") {
last.value += part.value;
} else {
valueParts.push({ type, value: part.value });
}
return valueParts;
}
return valueParts;
}
// default processing of numeric values

View File

@@ -31,18 +31,9 @@ type DialogSheetMode = "dialog" | "bottom-sheet";
* @slot footer - Dialog/sheet footer content.
*
* @cssprop --ha-dialog-surface-background - Dialog/sheet background color.
* @cssprop --ha-dialog-surface-backdrop-filter - Dialog/sheet backdrop filter.
* @cssprop --dialog-box-shadow - Dialog box shadow (dialog mode only).
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface (dialog mode only).
* @cssprop --ha-dialog-show-duration - Show animation duration (dialog mode only).
* @cssprop --ha-dialog-hide-duration - Hide animation duration (dialog mode only).
* @cssprop --ha-dialog-scrim-backdrop-filter - Dialog/sheet scrim backdrop filter.
* @cssprop --dialog-backdrop-filter - Dialog/sheet scrim backdrop filter (legacy).
* @cssprop --mdc-dialog-scrim-color - Dialog/sheet scrim color (legacy).
* @cssprop --ha-bottom-sheet-surface-background - Bottom sheet background color (sheet mode only).
* @cssprop --ha-bottom-sheet-surface-backdrop-filter - Bottom sheet backdrop filter (sheet mode only).
* @cssprop --ha-bottom-sheet-scrim-backdrop-filter - Bottom sheet scrim backdrop filter (sheet mode only).
* @cssprop --ha-bottom-sheet-scrim-color - Bottom sheet scrim color (sheet mode only).
*
* @attr {boolean} open - Controls the dialog/sheet open state.
* @attr {("alert"|"standard")} type - Dialog type (dialog mode only). Defaults to "standard".

View File

@@ -25,27 +25,6 @@ const SWIPE_LOCKED_COMPONENTS = new Set([
const SWIPE_LOCKED_CLASSES = new Set(["volume-slider-container", "forecast"]);
/**
* Home Assistant bottom sheet component.
*
* @element ha-bottom-sheet
* @extends {LitElement}
*
* @cssprop --ha-bottom-sheet-height - Preferred height of the bottom sheet.
* @cssprop --ha-bottom-sheet-max-height - Maximum height of the bottom sheet.
* @cssprop --ha-bottom-sheet-max-width - Maximum width of the bottom sheet.
* @cssprop --ha-bottom-sheet-border-radius - Top border radius of the bottom sheet.
* @cssprop --ha-bottom-sheet-surface-background - Bottom sheet background color.
* @cssprop --ha-bottom-sheet-surface-backdrop-filter - Bottom sheet surface backdrop filter.
* @cssprop --ha-bottom-sheet-scrim-backdrop-filter - Bottom sheet scrim backdrop filter.
* @cssprop --ha-bottom-sheet-scrim-color - Bottom sheet scrim color.
*
* @cssprop --ha-dialog-surface-background - Bottom sheet background color fallback.
* @cssprop --ha-dialog-surface-backdrop-filter - Bottom sheet surface backdrop filter fallback.
* @cssprop --ha-dialog-scrim-backdrop-filter - Bottom sheet scrim backdrop filter fallback.
* @cssprop --dialog-backdrop-filter - Bottom sheet scrim backdrop filter legacy fallback.
* @cssprop --mdc-dialog-scrim-color - Bottom sheet scrim color legacy fallback.
*/
@customElement("ha-bottom-sheet")
export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
@property({ attribute: false }) public hass?: HomeAssistant;
@@ -162,9 +141,6 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
private _handleKeyDown = (ev: KeyboardEvent) => {
if (ev.key === "Escape") {
this._escapePressed = true;
if (this.preventScrimClose) {
ev.preventDefault();
}
ev.stopPropagation();
(ev.currentTarget as WaDrawer).open = false;
}
@@ -406,26 +382,6 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
transform: var(--dialog-transform);
transition: var(--dialog-transition);
}
wa-drawer::part(dialog)::backdrop {
-webkit-backdrop-filter: var(
--ha-bottom-sheet-scrim-backdrop-filter,
var(
--ha-dialog-scrim-backdrop-filter,
var(--dialog-backdrop-filter, none)
)
);
backdrop-filter: var(
--ha-bottom-sheet-scrim-backdrop-filter,
var(
--ha-dialog-scrim-backdrop-filter,
var(--dialog-backdrop-filter, none)
)
);
background-color: var(
--ha-bottom-sheet-scrim-color,
var(--mdc-dialog-scrim-color, none)
);
}
wa-drawer::part(body) {
max-width: var(--ha-bottom-sheet-max-width);
width: 100%;
@@ -440,18 +396,7 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
);
background-color: var(
--ha-bottom-sheet-surface-background,
var(
--ha-dialog-surface-background,
var(--card-background-color, var(--ha-color-surface-default))
)
);
-webkit-backdrop-filter: var(
--ha-bottom-sheet-surface-backdrop-filter,
var(--ha-dialog-surface-backdrop-filter, none)
);
backdrop-filter: var(
--ha-bottom-sheet-surface-backdrop-filter,
var(--ha-dialog-surface-backdrop-filter, none)
var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
);
padding: var(
--ha-bottom-sheet-padding,

View File

@@ -245,7 +245,7 @@ export class HaButton extends Button {
}
.label {
overflow: var(--ha-button-label-overflow, hidden);
overflow: hidden;
text-overflow: ellipsis;
padding: var(--ha-space-1) 0;
}

View File

@@ -1,9 +1,11 @@
import { mdiMenuDown } from "@mdi/js";
import type { TemplateResult } from "lit";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import type { HomeAssistant } from "../types";
import "./ha-attribute-icon";
import "./ha-dropdown";
import "./ha-dropdown-item";
import "./ha-icon";
@@ -14,10 +16,17 @@ export interface SelectOption {
value: string;
iconPath?: string;
icon?: string;
attributeIcon?: {
stateObj: HassEntity;
attribute: string;
attributeValue?: string;
};
}
@customElement("ha-control-select-menu")
export class HaControlSelectMenu extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, attribute: "show-arrow" })
public showArrow = false;
@@ -38,9 +47,6 @@ export class HaControlSelectMenu extends LitElement {
@property({ attribute: false }) public options: SelectOption[] = [];
@property({ attribute: false })
public renderIcon?: (value: string) => TemplateResult<1> | typeof nothing;
@query("button") private _triggerButton!: HTMLButtonElement;
public override render() {
@@ -88,8 +94,14 @@ export class HaControlSelectMenu extends LitElement {
? html`<ha-svg-icon slot="icon" .path=${option.iconPath}></ha-svg-icon>`
: option.icon
? html`<ha-icon slot="icon" .icon=${option.icon}></ha-icon>`
: this.renderIcon
? html`<span slot="icon">${this.renderIcon(option.value)}</span>`
: option.attributeIcon
? html`<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${option.attributeIcon.stateObj}
.attribute=${option.attributeIcon.attribute}
.attributeValue=${option.attributeIcon.attributeValue}
></ha-attribute-icon>`
: nothing}
${option.label}</ha-dropdown-item
>`;
@@ -107,20 +119,24 @@ export class HaControlSelectMenu extends LitElement {
}
private _renderIcon() {
const value = this.getValueObject(this.options, this.value);
const { iconPath, icon, attributeIcon } =
this.getValueObject(this.options, this.value) ?? {};
const defaultIcon = this.querySelector("[slot='icon']");
return html`
<div class="icon">
${value?.iconPath
? html`<ha-svg-icon
slot="icon"
.path=${value.iconPath}
></ha-svg-icon>`
: value?.icon
? html`<ha-icon slot="icon" .icon=${value.icon}></ha-icon>`
: this.renderIcon && this.value
? this.renderIcon(this.value)
${iconPath
? html`<ha-svg-icon slot="icon" .path=${iconPath}></ha-svg-icon>`
: icon
? html`<ha-icon slot="icon" .icon=${icon}></ha-icon>`
: attributeIcon
? html`<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${attributeIcon.stateObj}
.attribute=${attributeIcon.attribute}
.attributeValue=${attributeIcon.attributeValue}
></ha-attribute-icon>`
: defaultIcon
? html`<slot name="icon"></slot>`
: nothing}
@@ -156,12 +172,12 @@ export class HaControlSelectMenu extends LitElement {
font-size: var(--ha-font-size-m);
line-height: 1.4;
width: auto;
color: var(--primary-text-color);
-webkit-tap-highlight-color: transparent;
}
.select-anchor {
border: none;
text-align: left;
color: var(--primary-text-color);
height: var(--control-select-menu-height);
padding: var(--control-select-menu-padding);
overflow: hidden;

View File

@@ -93,8 +93,6 @@ export class HaDateRangePicker extends LitElement {
| "center"
| "inline";
@state() private _calcedVerticalOpeningDirection?: "up" | "down";
protected willUpdate(changedProps: PropertyValues) {
if (
(!this.hasUpdated && this.ranges === undefined) ||
@@ -136,9 +134,7 @@ export class HaDateRangePicker extends LitElement {
opening-direction=${ifDefined(
this.openingDirection || this._calcedOpeningDirection
)}
opens-vertical=${ifDefined(
this.verticalOpeningDirection || this._calcedVerticalOpeningDirection
)}
opens-vertical=${ifDefined(this.verticalOpeningDirection)}
first-day=${firstWeekdayIndex(this.hass.locale)}
language=${this.hass.locale.language}
@change=${this._handleChange}
@@ -332,24 +328,17 @@ export class HaDateRangePicker extends LitElement {
private _handleClick() {
// calculate opening direction if not set
if (!this._dateRangePicker.open) {
if (!this.openingDirection) {
const datePickerPosition = this.getBoundingClientRect().x;
let opens: "right" | "left" | "center" | "inline";
if (datePickerPosition > (2 * window.innerWidth) / 3) {
opens = "left";
} else if (datePickerPosition < window.innerWidth / 3) {
opens = "right";
} else {
opens = "center";
}
this._calcedOpeningDirection = opens;
}
if (!this.verticalOpeningDirection) {
const rect = this.getBoundingClientRect();
this._calcedVerticalOpeningDirection =
rect.top > window.innerHeight / 2 ? "up" : "down";
if (!this._dateRangePicker.open && !this.openingDirection) {
const datePickerPosition = this.getBoundingClientRect().x;
let opens: "right" | "left" | "center" | "inline";
if (datePickerPosition > (2 * window.innerWidth) / 3) {
opens = "left";
} else if (datePickerPosition < window.innerWidth / 3) {
opens = "right";
} else {
opens = "center";
}
this._calcedOpeningDirection = opens;
}
}

View File

@@ -52,12 +52,7 @@ type DialogHideEvent = CustomEvent<{ source?: Element }>;
* @cssprop --ha-dialog-show-duration - Show animation duration.
* @cssprop --ha-dialog-hide-duration - Hide animation duration.
* @cssprop --ha-dialog-surface-background - Dialog background color.
* @cssprop --ha-dialog-surface-backdrop-filter - Dialog backdrop filter.
* @cssprop --dialog-box-shadow - Dialog box shadow.
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface.
* @cssprop --ha-dialog-scrim-backdrop-filter - Dialog scrim backdrop filter.
* @cssprop --dialog-backdrop-filter - Dialog scrim backdrop filter (legacy).
* @cssprop --mdc-dialog-scrim-color - Dialog scrim color (legacy).
* @cssprop --dialog-surface-margin-top - Top margin for the dialog surface.
*
* @attr {boolean} open - Controls the dialog open state.
@@ -244,9 +239,6 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
private _handleKeyDown(ev: KeyboardEvent) {
if (ev.key === "Escape") {
this._escapePressed = true;
if (this.preventScrimClose) {
ev.preventDefault();
}
ev.stopPropagation();
(ev.currentTarget as WaDialog).open = false;
}
@@ -276,6 +268,10 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
--spacing: var(--dialog-content-padding, var(--ha-space-6));
--show-duration: var(--ha-dialog-show-duration, 200ms);
--hide-duration: var(--ha-dialog-hide-duration, 200ms);
--ha-dialog-surface-background: var(
--card-background-color,
var(--ha-color-surface-default)
);
--wa-color-surface-raised: var(
--ha-dialog-surface-background,
var(--card-background-color, var(--ha-color-surface-default))
@@ -306,12 +302,6 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
}
wa-dialog::part(dialog) {
-webkit-backdrop-filter: var(
--ha-dialog-surface-backdrop-filter,
none
);
backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none);
box-shadow: var(--dialog-box-shadow, var(--wa-shadow-l));
color: var(--primary-text-color);
min-width: var(--width, var(--full-width));
max-width: var(--width, var(--full-width));
@@ -341,18 +331,6 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
overflow: hidden;
}
wa-dialog::part(dialog)::backdrop {
-webkit-backdrop-filter: var(
--ha-dialog-scrim-backdrop-filter,
var(--dialog-backdrop-filter, none)
);
backdrop-filter: var(
--ha-dialog-scrim-backdrop-filter,
var(--dialog-backdrop-filter, none)
);
background-color: var(--mdc-dialog-scrim-color, none);
}
@media all and (max-width: 450px), all and (max-height: 500px) {
:host([type="standard"]) {
--ha-dialog-border-radius: 0;

View File

@@ -37,9 +37,6 @@ export class HaIconButtonToggle extends HaIconButton {
background-color: transparent;
border: 2px solid var(--primary-text-color);
}
:host([selected]) ha-button::after {
opacity: 0;
}
:host([selected]) ha-button::part(base) {
color: var(--primary-background-color);
background-color: unset;

View File

@@ -74,7 +74,6 @@ export class HaIconButton extends LitElement {
);
--wa-color-on-normal: currentColor;
--wa-color-fill-quiet: transparent;
--ha-button-label-overflow: visible;
}
ha-button::after {
content: "";

View File

@@ -9,7 +9,6 @@ import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu";
import "../../../components/ha-icon-button-group";
import "../../../components/ha-icon-button-toggle";
@@ -40,38 +39,6 @@ class MoreInfoClimate extends LitElement {
@state() private _mainControl: MainControl = "temperature";
private _renderPresetModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="preset_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private _renderFanModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="fan_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private _renderSwingModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="swing_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private _renderSwingHorizontalModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="swing_horizontal_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
protected willUpdate(changedProps: PropertyValues): void {
if (
changedProps.has("stateObj") &&
@@ -238,8 +205,12 @@ class MoreInfoClimate extends LitElement {
"preset_mode",
mode
),
attributeIcon: {
stateObj,
attribute: "preset_mode",
attributeValue: mode,
},
}))}
.renderIcon=${this._renderPresetModeIcon}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
</ha-control-select-menu>
@@ -263,8 +234,12 @@ class MoreInfoClimate extends LitElement {
"fan_mode",
mode
),
attributeIcon: {
stateObj,
attribute: "fan_mode",
attributeValue: mode,
},
}))}
.renderIcon=${this._renderFanModeIcon}
>
<ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
</ha-control-select-menu>
@@ -288,8 +263,12 @@ class MoreInfoClimate extends LitElement {
"swing_mode",
mode
),
attributeIcon: {
stateObj,
attribute: "swing_mode",
attributeValue: mode,
},
}))}
.renderIcon=${this._renderSwingModeIcon}
>
<ha-svg-icon
slot="icon"
@@ -318,9 +297,13 @@ class MoreInfoClimate extends LitElement {
"swing_horizontal_mode",
mode
),
attributeIcon: {
stateObj,
attribute: "swing_horizontal_mode",
attributeValue: mode,
},
})
)}
.renderIcon=${this._renderSwingHorizontalModeIcon}
>
<ha-svg-icon
slot="icon"

View File

@@ -40,22 +40,6 @@ class MoreInfoFan extends LitElement {
@state() public _presetMode?: string;
private _renderPresetModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="preset_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private _renderDirectionIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="direction"
.attributeValue=${value}
></ha-attribute-icon>`;
private _toggle = () => {
const service = this.stateObj?.state === "on" ? "turn_off" : "turn_on";
forwardHaptic(this, "light");
@@ -208,9 +192,15 @@ class MoreInfoFan extends LitElement {
"preset_mode",
mode
),
attributeIcon: this.stateObj
? {
stateObj: this.stateObj,
attribute: "preset_mode",
attributeValue: mode,
}
: undefined,
})
)}
.renderIcon=${this._renderPresetModeIcon}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
</ha-control-select-menu>
@@ -236,8 +226,14 @@ class MoreInfoFan extends LitElement {
direction
)
: direction,
attributeIcon: this.stateObj
? {
stateObj: this.stateObj,
attribute: "direction",
attributeValue: direction,
}
: undefined,
}))}
.renderIcon=${this._renderDirectionIcon}
>
<ha-attribute-icon
slot="icon"

View File

@@ -23,14 +23,6 @@ class MoreInfoHumidifier extends LitElement {
@state() public _mode?: string;
private _renderModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="mode"
.attributeValue=${value}
></ha-attribute-icon>`;
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("stateObj")) {
@@ -114,8 +106,14 @@ class MoreInfoHumidifier extends LitElement {
mode
)
: mode,
attributeIcon: stateObj
? {
stateObj,
attribute: "mode",
attributeValue: mode,
}
: undefined,
})) || []}
.renderIcon=${this._renderModeIcon}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
</ha-control-select-menu>

View File

@@ -55,14 +55,6 @@ class MoreInfoLight extends LitElement {
@state() private _mainControl: MainControl = "brightness";
private _renderEffectIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="effect"
.attributeValue=${value}
></ha-attribute-icon>`;
protected updated(changedProps: PropertyValues<typeof this>): void {
if (changedProps.has("stateObj")) {
this._effect = this.stateObj?.attributes.effect;
@@ -279,9 +271,15 @@ class MoreInfoLight extends LitElement {
effect
)
: effect,
attributeIcon: this.stateObj
? {
stateObj: this.stateObj,
attribute: "effect",
attributeValue: effect,
}
: undefined,
})
)}
.renderIcon=${this._renderEffectIcon}
>
<ha-svg-icon slot="icon" .path=${mdiCreation}></ha-svg-icon>
</ha-control-select-menu>

View File

@@ -24,14 +24,6 @@ class MoreInfoWaterHeater extends LitElement {
@property({ attribute: false }) public stateObj?: WaterHeaterEntity;
private _renderOperationModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="operation_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
protected render() {
if (!this.stateObj) {
return nothing;
@@ -93,8 +85,12 @@ class MoreInfoWaterHeater extends LitElement {
.map((mode) => ({
value: mode,
label: this.hass.formatEntityState(stateObj, mode),
attributeIcon: {
stateObj,
attribute: "operation_mode",
attributeValue: mode,
},
}))}
.renderIcon=${this._renderOperationModeIcon}
>
<ha-svg-icon slot="icon" .path=${mdiWaterBoiler}></ha-svg-icon>
</ha-control-select-menu>

View File

@@ -2,27 +2,17 @@ import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeAttributeNameDisplay } from "../../common/entity/compute_attribute_display";
import checkValidDate from "../../common/datetime/check_valid_date";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import "../../components/ha-attribute-value";
import "../../components/ha-card";
import type { LocalizeKeys } from "../../common/translations/localize";
import { computeShownAttributes } from "../../data/entity/entity_attributes";
import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry";
import type { HomeAssistant } from "../../types";
import "../../components/ha-yaml-editor";
interface DetailsViewParams {
entityId: string;
}
interface DetailEntry {
translationKey: LocalizeKeys;
value: string;
}
@customElement("ha-more-info-details")
class HaMoreInfoDetails extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -31,8 +21,6 @@ class HaMoreInfoDetails extends LitElement {
@property({ attribute: false }) public params?: DetailsViewParams;
@property({ attribute: false }) public yamlMode = false;
@state() private _stateObj?: HassEntity;
protected willUpdate(changedProps: PropertyValues): void {
@@ -49,127 +37,60 @@ class HaMoreInfoDetails extends LitElement {
return nothing;
}
const { stateEntries, attributes, yamlData } = this._getDetailData(
this._stateObj
const translatedState = this.hass.formatEntityState(this._stateObj);
const detailsAttributes = computeShownAttributes(this._stateObj);
const detailsAttributeSet = new Set(detailsAttributes);
const builtInAttributes = Object.keys(this._stateObj.attributes).filter(
(attribute) => !detailsAttributeSet.has(attribute)
);
const allAttributes = [...detailsAttributes, ...builtInAttributes];
return html`
<div class="content">
${this.yamlMode
? html`<ha-yaml-editor
.hass=${this.hass}
.value=${yamlData}
read-only
auto-update
></ha-yaml-editor>`
: html`
<section class="section">
<h2 class="section-title">
${this.hass.localize(
"ui.components.entity.entity-state-picker.state"
)}
</h2>
<ha-card>
<div class="card-content">
<div class="data-group">
${stateEntries.map(
(entry) =>
html`<div class="data-entry">
<div class="key">
${this.hass.localize(entry.translationKey)}
</div>
<div class="value">${entry.value}</div>
</div>`
)}
</div>
<section class="section">
<h2 class="section-title">
${this.hass.localize(
"ui.components.entity.entity-state-picker.state"
)}
</h2>
<ha-card>
<div class="card-content">
<div class="attribute-group">
<div class="data-entry">
<div class="key">
${this.hass.localize(
"ui.dialogs.more_info_control.translated"
)}
</div>
</ha-card>
</section>
<div class="value">${translatedState}</div>
</div>
<div class="data-entry">
<div class="key">
${this.hass.localize("ui.dialogs.more_info_control.raw")}
</div>
<div class="value">${this._stateObj.state}</div>
</div>
</div>
</div>
</ha-card>
</section>
<section class="section">
<h2 class="section-title">
${this.hass.localize(
"ui.dialogs.more_info_control.attributes"
)}
</h2>
<ha-card>
<div class="card-content">
<div class="data-group">
${this._renderAttributes(attributes)}
</div>
</div>
</ha-card>
</section>
`}
<section class="section">
<h2 class="section-title">
${this.hass.localize("ui.dialogs.more_info_control.attributes")}
</h2>
<ha-card>
<div class="card-content">
<div class="attribute-group">
${this._renderAttributes(allAttributes)}
</div>
</div>
</ha-card>
</section>
</div>
`;
}
private _getDetailData = memoizeOne(
(
stateObj: HassEntity
): {
stateEntries: DetailEntry[];
attributes: string[];
yamlData: {
state: {
translated: string;
raw: string;
last_changed: string;
last_updated: string;
};
attributes: Record<string, string>;
};
} => {
const translatedState = this.hass.formatEntityState(stateObj);
const detailsAttributes = computeShownAttributes(stateObj);
const detailsAttributeSet = new Set(detailsAttributes);
const builtInAttributes = Object.keys(stateObj.attributes).filter(
(attribute) => !detailsAttributeSet.has(attribute)
);
return {
stateEntries: [
{
translationKey: "ui.dialogs.more_info_control.translated",
value: translatedState,
},
{
translationKey: "ui.dialogs.more_info_control.raw",
value: stateObj.state,
},
{
translationKey: "ui.dialogs.more_info_control.last_changed",
value: this._formatTimestamp(stateObj.last_changed),
},
{
translationKey: "ui.dialogs.more_info_control.last_updated",
value: this._formatTimestamp(stateObj.last_updated),
},
],
attributes: [...detailsAttributes, ...builtInAttributes],
yamlData: {
state: {
translated: translatedState,
raw: stateObj.state,
last_changed: stateObj.last_changed,
last_updated: stateObj.last_updated,
},
attributes: stateObj.attributes,
},
};
}
);
private _formatTimestamp(value: string): string {
const date = new Date(value);
return checkValidDate(date)
? formatDateTimeWithSeconds(date, this.hass.locale, this.hass.config)
: value;
}
private _renderAttributes(attributes: string[]) {
if (attributes.length === 0) {
return html`<div class="empty">
@@ -238,7 +159,7 @@ class HaMoreInfoDetails extends LitElement {
border-bottom: 1px solid var(--divider-color);
}
.data-group .data-entry:last-of-type {
.attribute-group .data-entry:last-of-type {
border-bottom: none;
}

View File

@@ -1,7 +1,6 @@
import {
mdiChartBoxOutline,
mdiClose,
mdiCodeBraces,
mdiCogOutline,
mdiDevices,
mdiDotsVertical,
@@ -133,8 +132,6 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
@state() private _infoEditMode = false;
@state() private _detailsYamlMode = false;
@state() private _isEscapeEnabled = true;
@state() private _sensorNumericDeviceClasses?: string[] = [];
@@ -185,7 +182,6 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
this._parentEntityIds = [];
this._entry = undefined;
this._infoEditMode = false;
this._detailsYamlMode = false;
this._initialView = DEFAULT_VIEW;
this._currView = DEFAULT_VIEW;
this._childView = undefined;
@@ -255,7 +251,6 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
private _goBack() {
if (this._childView) {
this._childView = undefined;
this._detailsYamlMode = false;
return;
}
if (this._initialView !== this._currView) {
@@ -319,10 +314,6 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
this._infoEditMode = !this._infoEditMode;
}
private _toggleDetailsYamlMode() {
this._detailsYamlMode = !this._detailsYamlMode;
}
private _handleToggleInfoEditModeEvent(ev) {
this._infoEditMode = ev.detail;
}
@@ -646,18 +637,7 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
</ha-dropdown-item>
</ha-dropdown>
`
: this._childView?.viewTag === "ha-more-info-details"
? html`
<ha-icon-button
slot="headerActionItems"
.label=${this.hass.localize(
"ui.dialogs.more_info_control.toggle_yaml_mode"
)}
.path=${mdiCodeBraces}
@click=${this._toggleDetailsYamlMode}
></ha-icon-button>
`
: nothing}
: nothing}
<div
class=${classMap({
"content-wrapper": true,
@@ -683,7 +663,6 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
hass: this.hass,
entry: this._entry,
params: this._childView.viewParams,
yamlMode: this._detailsYamlMode,
})}
</div>
`
@@ -752,7 +731,6 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
if (changedProps.has("_currView")) {
this._childView = undefined;
this._infoEditMode = false;
this._detailsYamlMode = false;
}
}

View File

@@ -288,7 +288,7 @@ export class QuickBar extends LitElement {
<ha-combo-box-item
tabindex="-1"
type="button"
style="--mdc-icon-size: 24px;"
style="--mdc-icon-size: 32px;"
>
${"stateObj" in item && item.stateObj
? html`
@@ -302,7 +302,6 @@ export class QuickBar extends LitElement {
? html`
<ha-domain-icon
slot="start"
style="margin: var(--ha-space-1);"
.hass=${this.hass}
.domain=${item.domain}
brand-fallback
@@ -320,11 +319,7 @@ export class QuickBar extends LitElement {
/>
`
: item.icon
? html`<ha-icon
style="margin: var(--ha-space-1);"
slot="start"
.icon=${item.icon}
></ha-icon>`
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: "iconColor" in item && item.iconColor
? html`
<div
@@ -338,11 +333,7 @@ export class QuickBar extends LitElement {
</div>
`
: html`
<ha-svg-icon
style="margin: var(--ha-space-1);"
slot="start"
.path=${iconPath}
></ha-svg-icon>
<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>
`}
<span slot="headline">${item.primary}</span>
${item.secondary

View File

@@ -27,19 +27,15 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { UndoRedoController } from "../../../common/controllers/undo-redo-controller";
import { transform } from "../../../common/decorators/transform";
import { fireEvent } from "../../../common/dom/fire_event";
import { goBack, navigate } from "../../../common/navigate";
import { promiseTimeout } from "../../../common/util/promise-timeout";
import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-fade-in";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-spinner";
import "../../../components/ha-svg-icon";
import "../../../components/ha-yaml-editor";
import type {
@@ -72,27 +68,22 @@ import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import "../../../layouts/hass-subpage";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
import { haStyle } from "../../../resources/styles";
import type {
Entries,
HomeAssistant,
Route,
ValueChangedEvent,
} from "../../../types";
import type { Entries, ValueChangedEvent } from "../../../types";
import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode";
import {
type EntityRegistryUpdate,
showAutomationSaveDialog,
} from "./automation-save-dialog/show-dialog-automation-save";
import { showAutomationSaveDialog } from "./automation-save-dialog/show-dialog-automation-save";
import { showAutomationSaveTimeoutDialog } from "./automation-save-timeout-dialog/show-dialog-automation-save-timeout";
import "./blueprint-automation-editor";
import {
AutomationScriptEditorMixin,
automationScriptEditorStyles,
} from "./ha-automation-script-editor-mixin";
import "./manual-automation-editor";
import type { HaManualAutomationEditor } from "./manual-automation-editor";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@@ -119,53 +110,13 @@ declare global {
}
@customElement("ha-automation-editor")
export class HaAutomationEditor extends PreventUnsavedMixin(
KeyboardShortcutMixin(LitElement)
export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationConfig>(
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public automationId: string | null = null;
@property({ attribute: false }) public entityId: string | null = null;
@property({ attribute: false }) public automations!: AutomationEntity[];
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@state() private _config?: AutomationConfig;
@state() private _dirty = false;
@state() private _errors?: string;
@state() private _yamlErrors?: string;
@state() private _entityId?: string;
@state() private _mode: "gui" | "yaml" = "gui";
@state() private _readOnly = false;
@state() private _validationErrors?: (string | TemplateResult)[];
@state() private _blueprintConfig?: BlueprintAutomationConfig;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
transformer: function (this: HaAutomationEditor, value) {
return value.find(({ entity_id }) => entity_id === this._entityId);
},
watch: ["_entityId"],
})
private _registryEntry?: EntityRegistryEntry;
@state() private _saving = false;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
_entityRegistry!: EntityRegistryEntry[];
@@ -180,24 +131,18 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
private _configSubscriptionsId = 1;
private _entityRegistryUpdate?: EntityRegistryUpdate;
private _newAutomationId?: string;
private _entityRegCreated?: (
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
) => void;
private _undoRedoController = new UndoRedoController<AutomationConfig>(this, {
apply: (config) => this._applyUndoRedo(config),
currentConfig: () => this._config!,
currentConfig: () => this.config!,
});
protected willUpdate(changedProps) {
super.willUpdate(changedProps);
if (
this._entityRegCreated &&
this.entityRegCreated &&
this._newAutomationId &&
changedProps.has("_entityRegistry")
) {
@@ -207,26 +152,22 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
entity.unique_id === this._newAutomationId
);
if (automation) {
this._entityRegCreated(automation);
this._entityRegCreated = undefined;
this.entityRegCreated(automation);
this.entityRegCreated = undefined;
}
}
}
protected render(): TemplateResult | typeof nothing {
if (!this._config) {
return html`
<ha-fade-in .delay=${500}>
<ha-spinner size="large"></ha-spinner>
</ha-fade-in>
`;
if (!this.config) {
return this.renderLoading();
}
const stateObj = this._entityId
? this.hass.states[this._entityId]
const stateObj = this.currentEntityId
? this.hass.states[this.currentEntityId]
: undefined;
const useBlueprint = "use_blueprint" in this._config;
const useBlueprint = "use_blueprint" in this.config;
const shortcutIcon = isMac
? html`<ha-svg-icon .path=${mdiAppleKeyboardCommand}></ha-svg-icon>`
: this.hass.localize("ui.panel.config.automation.editor.ctrl");
@@ -236,11 +177,11 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.backCallback=${this._backTapped}
.header=${this._config.alias ||
.backCallback=${this.backTapped}
.header=${this.config.alias ||
this.hass.localize("ui.panel.config.automation.editor.default_name")}
>
${this._mode === "gui" && !this.narrow
${this.mode === "gui" && !this.narrow
? html`<ha-icon-button
slot="toolbar-icon"
.label=${this.hass.localize("ui.common.undo")}
@@ -284,7 +225,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
</span>
</ha-tooltip>`
: nothing}
${this._config?.id && !this.narrow
${this.config?.id && !this.narrow
? html`
<ha-button
appearance="plain"
@@ -308,7 +249,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.path=${mdiDotsVertical}
></ha-icon-button>
${this._mode === "gui" && this.narrow
${this.mode === "gui" && this.narrow
? html`<ha-dropdown-item
value="undo"
.disabled=${!this._undoRedoController.canUndo}
@@ -342,7 +283,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
<ha-dropdown-item .disabled=${!stateObj} value="category">
${this.hass.localize(
`ui.panel.config.scene.picker.${this._registryEntry?.categories?.automation ? "edit_category" : "assign_category"}`
`ui.panel.config.scene.picker.${this.registryEntry?.categories?.automation ? "edit_category" : "assign_category"}`
)}
<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>
</ha-dropdown-item>
@@ -366,9 +307,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
<ha-dropdown-item
value="rename"
.disabled=${this._readOnly ||
.disabled=${this.readOnly ||
!this.automationId ||
this._mode === "yaml"}
this.mode === "yaml"}
>
${this.hass.localize("ui.panel.config.automation.editor.rename")}
<ha-svg-icon slot="icon" .path=${mdiRenameBox}></ha-svg-icon>
@@ -377,7 +318,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
? html`
<ha-dropdown-item
@click=${this._promptAutomationMode}
.disabled=${this._readOnly || this._mode === "yaml"}
.disabled=${this.readOnly || this.mode === "yaml"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.change_mode"
@@ -391,12 +332,12 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
: nothing}
<ha-dropdown-item
.disabled=${!!this._blueprintConfig ||
(!this._readOnly && !this.automationId)}
.disabled=${!!this.blueprintConfig ||
(!this.readOnly && !this.automationId)}
value="duplicate"
>
${this.hass.localize(
this._readOnly
this.readOnly
? "ui.panel.config.automation.editor.migrate"
: "ui.panel.config.automation.editor.duplicate"
)}
@@ -410,7 +351,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
? html`
<ha-dropdown-item
value="take_control"
.disabled=${this._readOnly}
.disabled=${this.readOnly}
>
${this.hass.localize(
"ui.panel.config.automation.editor.take_control"
@@ -422,7 +363,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
<ha-dropdown-item value="toggle_yaml_mode">
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${this._mode === "gui" ? "yaml" : "ui"}`
`ui.panel.config.automation.editor.edit_${this.mode === "gui" ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-dropdown-item>
@@ -456,10 +397,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
</ha-dropdown-item>
</ha-dropdown>
<div
class=${this._mode === "yaml" ? "yaml-mode" : ""}
class=${this.mode === "yaml" ? "yaml-mode" : ""}
@subscribe-automation-config=${this._subscribeAutomationConfig}
>
${this._mode === "gui"
${this.mode === "gui"
? html`
<div>
${useBlueprint
@@ -469,10 +410,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.narrow=${this.narrow}
.isWide=${this.isWide}
.stateObj=${stateObj}
.config=${this._config}
.disabled=${this._readOnly}
.saving=${this._saving}
.dirty=${this._dirty}
.config=${this.config}
.disabled=${this.readOnly}
.saving=${this.saving}
.dirty=${this.dirty}
@value-changed=${this._valueChanged}
@save-automation=${this._handleSaveAutomation}
></blueprint-automation-editor>
@@ -483,16 +424,16 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.narrow=${this.narrow}
.isWide=${this.isWide}
.stateObj=${stateObj}
.config=${this._config}
.disabled=${this._readOnly}
.dirty=${this._dirty}
.saving=${this._saving}
.config=${this.config}
.disabled=${this.readOnly}
.dirty=${this.dirty}
.saving=${this.saving}
@value-changed=${this._valueChanged}
@save-automation=${this._handleSaveAutomation}
@editor-save=${this._handleSaveAutomation}
>
<div class="alert-wrapper" slot="alerts">
${this._errors || stateObj?.state === UNAVAILABLE
${this.errors || stateObj?.state === UNAVAILABLE
? html`<ha-alert
alert-type="error"
.title=${stateObj?.state === UNAVAILABLE
@@ -501,7 +442,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
)
: undefined}
>
${this._errors || this._validationErrors}
${this.errors || this.validationErrors}
${stateObj?.state === UNAVAILABLE
? html`<ha-svg-icon
slot="icon"
@@ -510,7 +451,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
: nothing}
</ha-alert>`
: nothing}
${this._blueprintConfig
${this.blueprintConfig
? html`<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.automation.editor.confirm_take_control"
@@ -518,21 +459,21 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
<div slot="action" style="display: flex;">
<ha-button
appearance="plain"
@click=${this._takeControlSave}
@click=${this.takeControlSave}
>${this.hass.localize(
"ui.common.yes"
)}</ha-button
>
<ha-button
appearance="plain"
@click=${this._revertBlueprint}
@click=${this.revertBlueprint}
>${this.hass.localize(
"ui.common.no"
)}</ha-button
>
</div>
</ha-alert>`
: this._readOnly
: this.readOnly
? html`<ha-alert
alert-type="warning"
dismissable
@@ -575,7 +516,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
`}
</div>
`
: this._mode === "yaml"
: this.mode === "yaml"
? html`${stateObj?.state === "off"
? html`
<ha-alert alert-type="info">
@@ -598,7 +539,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._preprocessYaml()}
.readOnly=${this._readOnly}
.readOnly=${this.readOnly}
@value-changed=${this._yamlChanged}
@editor-save=${this._handleSaveAutomation}
.showErrors=${false}
@@ -606,9 +547,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
></ha-yaml-editor>
<ha-fab
slot="fab"
class=${this._dirty ? "dirty" : ""}
class=${this.dirty ? "dirty" : ""}
.label=${this.hass.localize("ui.common.save")}
.disabled=${this._saving}
.disabled=${this.saving}
extended
@click=${this._handleSaveAutomation}
>
@@ -645,7 +586,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
this.hass
) {
const initData = getAutomationEditorInitData();
this._dirty = !!initData;
this.dirty = !!initData;
let baseConfig: Partial<AutomationConfig> = { description: "" };
if (!initData || !("use_blueprint" in initData)) {
baseConfig = {
@@ -656,35 +597,35 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
actions: [],
};
}
this._config = {
this.config = {
...baseConfig,
...(initData ? normalizeAutomationConfig(initData) : initData),
} as AutomationConfig;
this._entityId = undefined;
this._readOnly = false;
this.currentEntityId = undefined;
this.readOnly = false;
}
if (changedProps.has("entityId") && this.entityId) {
getAutomationStateConfig(this.hass, this.entityId).then((c) => {
this._config = normalizeAutomationConfig(c.config);
this.config = normalizeAutomationConfig(c.config);
this._checkValidation();
});
this._entityId = this.entityId;
this._dirty = false;
this._readOnly = true;
this.currentEntityId = this.entityId;
this.dirty = false;
this.readOnly = true;
}
if (
changedProps.has("automations") &&
this.automationId &&
!this._entityId
!this.currentEntityId
) {
this._setEntityId();
}
if (changedProps.has("_config")) {
if (changedProps.has("config")) {
Object.values(this._configSubscriptions).forEach((sub) =>
sub(this._config)
sub(this.config)
);
}
}
@@ -693,24 +634,24 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
const automation = this.automations.find(
(entity: AutomationEntity) => entity.attributes.id === this.automationId
);
this._entityId = automation?.entity_id;
this.currentEntityId = automation?.entity_id;
}
private async _checkValidation() {
this._validationErrors = undefined;
if (!this._entityId || !this._config) {
this.validationErrors = undefined;
if (!this.currentEntityId || !this.config) {
return;
}
const stateObj = this.hass.states[this._entityId];
const stateObj = this.hass.states[this.currentEntityId];
if (stateObj?.state !== UNAVAILABLE) {
return;
}
const validation = await validateConfig(this.hass, {
triggers: this._config.triggers,
conditions: this._config.conditions,
actions: this._config.actions,
triggers: this.config.triggers,
conditions: this.config.conditions,
actions: this.config.actions,
});
this._validationErrors = (
this.validationErrors = (
Object.entries(validation) as Entries<typeof validation>
).map(([key, value]) =>
value.valid
@@ -728,9 +669,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
this.hass,
this.automationId as string
);
this._dirty = false;
this._readOnly = false;
this._config = normalizeAutomationConfig(config);
this.dirty = false;
this.readOnly = false;
this.config = normalizeAutomationConfig(config);
this._checkValidation();
} catch (err: any) {
const entity = this._entityRegistry.find(
@@ -761,34 +702,27 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
private _valueChanged(ev: ValueChangedEvent<AutomationConfig>) {
ev.stopPropagation();
if (this._config) {
this._undoRedoController.commit(this._config);
if (this.config) {
this._undoRedoController.commit(this.config);
}
this._config = ev.detail.value;
if (this._readOnly) {
this.config = ev.detail.value;
if (this.readOnly) {
return;
}
this._dirty = true;
this._errors = undefined;
this.dirty = true;
this.errors = undefined;
}
private _showInfo() {
if (!this.hass || !this._entityId) {
if (!this.hass || !this.currentEntityId) {
return;
}
fireEvent(this, "hass-more-info", { entityId: this._entityId });
}
private _showSettings() {
showMoreInfoDialog(this, {
entityId: this._entityId!,
view: "settings",
});
fireEvent(this, "hass-more-info", { entityId: this.currentEntityId });
}
private _editCategory() {
if (!this._registryEntry) {
if (!this.registryEntry) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.scene.picker.no_category_support"
@@ -801,36 +735,36 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
}
showAssignCategoryDialog(this, {
scope: "automation",
entityReg: this._registryEntry,
entityReg: this.registryEntry,
});
}
private async _showTrace() {
if (this._config?.id) {
const result = await this._confirmUnsavedChanged();
if (this.config?.id) {
const result = await this.confirmUnsavedChanged();
if (result) {
navigate(
`/config/automation/trace/${encodeURIComponent(this._config.id)}`
`/config/automation/trace/${encodeURIComponent(this.config.id)}`
);
}
}
}
private _runActions() {
if (!this.hass || !this._entityId) {
if (!this.hass || !this.currentEntityId) {
return;
}
triggerAutomationActions(
this.hass,
this.hass.states[this._entityId].entity_id
this.hass.states[this.currentEntityId].entity_id
);
}
private async _toggle(): Promise<void> {
if (!this.hass || !this._entityId) {
if (!this.hass || !this.currentEntityId) {
return;
}
const stateObj = this.hass.states[this._entityId];
const stateObj = this.hass.states[this.currentEntityId];
const service = stateObj.state === "off" ? "turn_on" : "turn_off";
await this.hass.callService("automation", service, {
entity_id: stateObj.entity_id,
@@ -838,42 +772,42 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
}
private _preprocessYaml() {
if (!this._config) {
if (!this.config) {
return {};
}
const cleanConfig: AutomationConfig = { ...this._config };
const cleanConfig: AutomationConfig = { ...this.config };
delete cleanConfig.id;
return cleanConfig;
}
private _yamlChanged(ev: CustomEvent) {
ev.stopPropagation();
this._dirty = true;
this.dirty = true;
if (!ev.detail.isValid) {
this._yamlErrors = ev.detail.errorMsg;
this.yamlErrors = ev.detail.errorMsg;
return;
}
this._yamlErrors = undefined;
this._config = {
id: this._config?.id,
this.yamlErrors = undefined;
this.config = {
id: this.config?.id,
...normalizeAutomationConfig(ev.detail.value),
};
this._errors = undefined;
this.errors = undefined;
}
private async _confirmUnsavedChanged(): Promise<boolean> {
if (!this._dirty) {
protected async confirmUnsavedChanged(): Promise<boolean> {
if (!this.dirty) {
return true;
}
return new Promise<boolean>((resolve) => {
showAutomationSaveDialog(this, {
config: this._config!,
config: this.config!,
domain: "automation",
updateConfig: async (config, entityRegistryUpdate) => {
this._config = config;
this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true;
this.config = config;
this.entityRegistryUpdate = entityRegistryUpdate;
this.dirty = true;
this.requestUpdate();
const id = this.automationId || String(Date.now());
@@ -889,8 +823,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
},
onClose: () => resolve(false),
onDiscard: () => resolve(true),
entityRegistryUpdate: this._entityRegistryUpdate,
entityRegistryEntry: this._registryEntry,
entityRegistryUpdate: this.entityRegistryUpdate,
entityRegistryEntry: this.registryEntry,
title: this.hass.localize(
this.automationId
? "ui.panel.config.automation.editor.leave.unsaved_confirm_title"
@@ -906,15 +840,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
});
}
private _backTapped = async () => {
const result = await this._confirmUnsavedChanged();
if (result) {
afterNextRender(() => goBack("/config"));
}
};
private async _takeControl() {
const config = this._config as BlueprintAutomationConfig;
const config = this.config as BlueprintAutomationConfig;
try {
const result = await substituteBlueprint(
@@ -931,35 +858,20 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
description: config.description,
};
this._blueprintConfig = config;
this._config = newConfig;
if (this._mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
this.blueprintConfig = config;
this.config = newConfig;
if (this.mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this.config);
}
this._readOnly = true;
this._errors = undefined;
this.readOnly = true;
this.errors = undefined;
} catch (err: any) {
this._errors = err.message;
this.errors = err.message;
}
}
private _revertBlueprint() {
this._config = this._blueprintConfig;
if (this._mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
}
this._blueprintConfig = undefined;
this._readOnly = false;
}
private _takeControlSave() {
this._readOnly = false;
this._dirty = true;
this._blueprintConfig = undefined;
}
private async _duplicate() {
const result = this._readOnly
const result = this.readOnly
? await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.picker.migrate_automation"
@@ -968,12 +880,12 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
"ui.panel.config.automation.picker.migrate_automation_description"
),
})
: await this._confirmUnsavedChanged();
: await this.confirmUnsavedChanged();
if (result) {
showAutomationEditor({
...this._config,
...this.config,
id: undefined,
alias: this._readOnly ? this._config?.alias : undefined,
alias: this.readOnly ? this.config?.alias : undefined,
});
}
}
@@ -985,7 +897,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
),
text: this.hass.localize(
"ui.panel.config.automation.picker.delete_confirm_text",
{ name: this._config?.alias }
{ name: this.config?.alias }
),
confirmText: this.hass!.localize("ui.common.delete"),
destructive: true,
@@ -1001,43 +913,21 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
}
}
private async _switchUiMode() {
if (this._yamlErrors) {
const result = await showConfirmationDialog(this, {
text: html`${this.hass.localize(
"ui.panel.config.automation.editor.switch_ui_yaml_error"
)}<br /><br />${this._yamlErrors}`,
confirmText: this.hass!.localize("ui.common.continue"),
destructive: true,
dismissText: this.hass!.localize("ui.common.cancel"),
});
if (!result) {
return;
}
}
this._yamlErrors = undefined;
this._mode = "gui";
}
private _switchYamlMode() {
this._mode = "yaml";
}
private async _promptAutomationAlias(): Promise<boolean> {
return new Promise((resolve) => {
showAutomationSaveDialog(this, {
config: this._config!,
config: this.config!,
domain: "automation",
updateConfig: async (config, entityRegistryUpdate) => {
this._config = config;
this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true;
this.config = config;
this.entityRegistryUpdate = entityRegistryUpdate;
this.dirty = true;
this.requestUpdate();
resolve(true);
},
onClose: () => resolve(false),
entityRegistryUpdate: this._entityRegistryUpdate,
entityRegistryEntry: this._registryEntry,
entityRegistryUpdate: this.entityRegistryUpdate,
entityRegistryEntry: this.registryEntry,
});
});
}
@@ -1045,10 +935,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
private async _promptAutomationMode(): Promise<void> {
return new Promise((resolve) => {
showAutomationModeDialog(this, {
config: this._config!,
config: this.config!,
updateConfig: (config) => {
this._config = config;
this._dirty = true;
this.config = config;
this.dirty = true;
this.requestUpdate();
resolve();
},
@@ -1058,9 +948,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
}
private async _handleSaveAutomation(): Promise<void> {
if (this._yamlErrors) {
if (this.yamlErrors) {
showToast(this, {
message: this._yamlErrors,
message: this.yamlErrors,
});
return;
}
@@ -1082,22 +972,22 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
}
private async _saveAutomation(id): Promise<void> {
this._saving = true;
this._validationErrors = undefined;
this.saving = true;
this.validationErrors = undefined;
let entityRegPromise: Promise<EntityRegistryEntry> | undefined;
if (this._entityRegistryUpdate !== undefined && !this._entityId) {
if (this.entityRegistryUpdate !== undefined && !this.currentEntityId) {
this._newAutomationId = id;
entityRegPromise = new Promise<EntityRegistryEntry>((resolve) => {
this._entityRegCreated = resolve;
this.entityRegCreated = resolve;
});
}
try {
await saveAutomationConfig(this.hass, id, this._config!);
await saveAutomationConfig(this.hass, id, this.config!);
if (this._entityRegistryUpdate !== undefined) {
let entityId = this._entityId;
if (this.entityRegistryUpdate !== undefined) {
let entityId = this.currentEntityId;
// wait for automation to appear in entity registry when creating a new automation
if (entityRegPromise) {
@@ -1131,23 +1021,23 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
if (entityId) {
await updateEntityRegistryEntry(this.hass, entityId, {
categories: {
automation: this._entityRegistryUpdate.category || null,
automation: this.entityRegistryUpdate.category || null,
},
labels: this._entityRegistryUpdate.labels || [],
area_id: this._entityRegistryUpdate.area || null,
labels: this.entityRegistryUpdate.labels || [],
area_id: this.entityRegistryUpdate.area || null,
});
}
}
this._dirty = false;
this.dirty = false;
} catch (errors: any) {
this._errors = errors.body?.message || errors.error || errors.body;
this.errors = errors.body?.message || errors.error || errors.body;
showToast(this, {
message: errors.body?.message || errors.error || errors.body,
});
throw errors;
} finally {
this._saving = false;
this.saving = false;
}
}
@@ -1157,7 +1047,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
ev.detail.unsub = () => {
delete this._configSubscriptions[id];
};
ev.detail.callback(this._config);
ev.detail.callback(this.config);
}
protected supportedShortcuts(): SupportedShortcuts {
@@ -1173,14 +1063,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
};
}
protected get isDirty() {
return this._dirty;
}
protected async promptDiscardChanges() {
return this._confirmUnsavedChanged();
}
// @ts-ignore
private _collapseAll() {
this._manualEditor?.collapseAll();
@@ -1205,8 +1087,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
private _applyUndoRedo(config: AutomationConfig) {
this._manualEditor?.triggerCloseSidebar();
this._config = config;
this._dirty = true;
this.config = config;
this.dirty = true;
}
private _undo() {
@@ -1235,7 +1117,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
this._showInfo();
break;
case "settings":
this._showSettings();
this.showSettings();
break;
case "category":
this._editCategory();
@@ -1256,11 +1138,11 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
this._takeControl();
break;
case "toggle_yaml_mode":
if (this._mode === "gui") {
this._switchYamlMode();
if (this.mode === "gui") {
this.switchYamlMode();
break;
}
this._switchUiMode();
this.switchUiMode();
break;
case "disable":
this._toggle();
@@ -1277,25 +1159,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
static get styles(): CSSResultGroup {
return [
haStyle,
automationScriptEditorStyles,
css`
:host {
--ha-automation-editor-max-width: var(
--ha-automation-editor-width,
1540px
);
}
ha-fade-in {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.yaml-mode {
height: 100%;
display: flex;
flex-direction: column;
padding-bottom: 0;
}
manual-automation-editor,
blueprint-automation-editor {
margin: 0 auto;
@@ -1309,17 +1174,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
padding: 0 12px;
}
ha-yaml-editor {
flex-grow: 1;
--actions-border-radius: var(--ha-border-radius-square);
--code-mirror-height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
}
p {
margin-bottom: 0;
}
ha-entity-toggle {
margin-right: 8px;
margin-inline-end: 8px;
@@ -1335,24 +1189,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
max-width: 1040px;
padding: 28px 20px 0;
}
ha-fab {
position: fixed;
right: calc(16px + var(--safe-area-inset-right, 0px));
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
}
ha-fab.dirty {
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
}
ha-tooltip ha-svg-icon {
width: 12px;
}
ha-tooltip .shortcut {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 2px;
}
`,
];
}

View File

@@ -0,0 +1,199 @@
import { consume } from "@lit/context";
import type { CSSResult, TemplateResult, LitElement } from "lit";
import { css, html } from "lit";
import { property, state } from "lit/decorators";
import { transform } from "../../../common/decorators/transform";
import { goBack } from "../../../common/navigate";
import { afterNextRender } from "../../../common/util/render-status";
import { fullEntitiesContext } from "../../../data/context";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import type { Constructor, HomeAssistant, Route } from "../../../types";
import type { EntityRegistryUpdate } from "./automation-save-dialog/show-dialog-automation-save";
import "../../../components/ha-fade-in";
import "../../../components/ha-spinner"; // used by renderLoading() provided to both editors
/** Minimum config shape shared by both AutomationConfig and ScriptConfig. */
interface BaseEditorConfig {
alias?: string;
}
/** Shared CSS styles for both automation and script editors. */
export const automationScriptEditorStyles: CSSResult = css`
:host {
--ha-automation-editor-max-width: var(--ha-automation-editor-width, 1540px);
}
ha-fade-in {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.yaml-mode {
height: 100%;
display: flex;
flex-direction: column;
padding-bottom: 0;
}
ha-yaml-editor {
flex-grow: 1;
--actions-border-radius: var(--ha-border-radius-square);
--code-mirror-height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
}
p {
margin-bottom: 0;
}
ha-fab {
position: fixed;
right: calc(16px + var(--safe-area-inset-right, 0px));
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
}
ha-fab.dirty {
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
}
ha-tooltip ha-svg-icon {
width: 12px;
}
ha-tooltip .shortcut {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 2px;
}
`;
export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
superClass: Constructor<LitElement>
) => {
class AutomationScriptEditorClass extends superClass {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public entityId: string | null = null;
@state() protected dirty = false;
@state() protected errors?: string;
@state() protected yamlErrors?: string;
@state() protected currentEntityId?: string;
@state() protected mode: "gui" | "yaml" = "gui";
@state() protected readOnly = false;
@state() protected saving = false;
@state() protected validationErrors?: (string | TemplateResult)[];
@state() protected config?: TConfig;
@state() protected blueprintConfig?: TConfig;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
transformer: function (this: { currentEntityId?: string }, value) {
return value.find(
({ entity_id }) => entity_id === this.currentEntityId
);
},
watch: ["currentEntityId"],
})
protected registryEntry?: EntityRegistryEntry;
protected entityRegistryUpdate?: EntityRegistryUpdate;
protected entityRegCreated?: (
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
) => void;
protected renderLoading(): TemplateResult {
return html`
<ha-fade-in .delay=${500}>
<ha-spinner size="large"></ha-spinner>
</ha-fade-in>
`;
}
protected showSettings() {
showMoreInfoDialog(this, {
entityId: this.currentEntityId!,
view: "settings",
});
}
protected async switchUiMode() {
if (this.yamlErrors) {
const result = await showConfirmationDialog(this, {
text: html`${this.hass.localize(
"ui.panel.config.automation.editor.switch_ui_yaml_error"
)}<br /><br />${this.yamlErrors}`,
confirmText: this.hass!.localize("ui.common.continue"),
destructive: true,
dismissText: this.hass!.localize("ui.common.cancel"),
});
if (!result) {
return;
}
}
this.yamlErrors = undefined;
this.mode = "gui";
}
protected switchYamlMode() {
this.mode = "yaml";
}
protected takeControlSave() {
this.readOnly = false;
this.dirty = true;
this.blueprintConfig = undefined;
}
protected revertBlueprint() {
this.config = this.blueprintConfig;
if (this.mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this.config);
}
this.blueprintConfig = undefined;
this.readOnly = false;
}
protected backTapped = async () => {
const result = await this.confirmUnsavedChanged();
if (result) {
afterNextRender(() => goBack("/config"));
}
};
protected get isDirty() {
return this.dirty;
}
protected async promptDiscardChanges() {
return this.confirmUnsavedChanged();
}
/**
* Asks whether unsaved changes should be discarded.
* Subclasses must override this to show a confirmation dialog.
* @returns true to proceed (discard/save changes), false to cancel.
*/
protected confirmUnsavedChanged(): Promise<boolean> {
return Promise.resolve(true);
}
}
return AutomationScriptEditorClass;
};

View File

@@ -36,7 +36,7 @@ import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart";
import { showShortcutsDialog } from "../../../dialogs/shortcuts/show-shortcuts-dialog";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import { haStyle, haStyleScrollbar } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { isMac } from "../../../util/is_mac";
@@ -255,88 +255,90 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
</ha-dropdown-item>
</ha-dropdown>
<ha-config-section
.narrow=${this.narrow}
.isWide=${this.isWide}
full-width
>
${repairsIssues.length || canInstallUpdates.length
? html`<ha-card outlined>
${repairsIssues.length
? html`
<ha-config-repairs
<div class="content ha-scrollbar">
<ha-config-section
.narrow=${this.narrow}
.isWide=${this.isWide}
full-width
>
${repairsIssues.length || canInstallUpdates.length
? html`<ha-card outlined>
${repairsIssues.length
? html`
<ha-config-repairs
.hass=${this.hass}
.narrow=${this.narrow}
.total=${totalRepairIssues}
.repairsIssues=${repairsIssues}
></ha-config-repairs>
${totalRepairIssues > repairsIssues.length
? html`
<ha-assist-chip
href="/config/repairs"
.label=${this.hass.localize(
"ui.panel.config.repairs.more_repairs",
{
count:
totalRepairIssues - repairsIssues.length,
}
)}
>
</ha-assist-chip>
`
: ""}
`
: ""}
${repairsIssues.length && canInstallUpdates.length
? html`<hr />`
: ""}
${canInstallUpdates.length
? html`
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.total=${totalUpdates}
.updateEntities=${canInstallUpdates}
.isInstallable=${true}
></ha-config-updates>
${totalUpdates > canInstallUpdates.length
? html`
<ha-assist-chip
href="/config/updates"
label=${this.hass.localize(
"ui.panel.config.updates.more_updates",
{
count:
totalUpdates - canInstallUpdates.length,
}
)}
>
</ha-assist-chip>
`
: ""}
`
: ""}
</ha-card>`
: ""}
${this._pages(
this.cloudStatus,
isComponentLoaded(this.hass, "cloud"),
this.hass.auth.external?.config.hasSettingsScreen
).map((categoryPages) =>
categoryPages.length === 0
? nothing
: html`
<ha-card outlined>
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.total=${totalRepairIssues}
.repairsIssues=${repairsIssues}
></ha-config-repairs>
${totalRepairIssues > repairsIssues.length
? html`
<ha-assist-chip
href="/config/repairs"
.label=${this.hass.localize(
"ui.panel.config.repairs.more_repairs",
{
count:
totalRepairIssues - repairsIssues.length,
}
)}
>
</ha-assist-chip>
`
: ""}
`
: ""}
${repairsIssues.length && canInstallUpdates.length
? html`<hr />`
: ""}
${canInstallUpdates.length
? html`
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.total=${totalUpdates}
.updateEntities=${canInstallUpdates}
.isInstallable=${true}
></ha-config-updates>
${totalUpdates > canInstallUpdates.length
? html`
<ha-assist-chip
href="/config/updates"
label=${this.hass.localize(
"ui.panel.config.updates.more_updates",
{
count:
totalUpdates - canInstallUpdates.length,
}
)}
>
</ha-assist-chip>
`
: ""}
`
: ""}
</ha-card>`
: ""}
${this._pages(
this.cloudStatus,
isComponentLoaded(this.hass, "cloud"),
this.hass.auth.external?.config.hasSettingsScreen
).map((categoryPages) =>
categoryPages.length === 0
? nothing
: html`
<ha-card outlined>
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.pages=${categoryPages}
></ha-config-navigation>
</ha-card>
`
)}
<ha-tip .hass=${this.hass}>${this._tip}</ha-tip>
</ha-config-section>
.pages=${categoryPages}
></ha-config-navigation>
</ha-card>
`
)}
<ha-tip .hass=${this.hass}>${this._tip}</ha-tip>
</ha-config-section>
</div>
</ha-top-app-bar-fixed>
`;
}
@@ -392,7 +394,36 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleScrollbar,
css`
:host {
display: block;
height: 100%;
}
ha-top-app-bar-fixed {
height: 100%;
overflow: hidden;
}
.content {
height: calc(
100vh - var(--header-height, 0px) - var(
--safe-area-inset-top,
0px
) - var(--safe-area-inset-bottom, 0px)
);
height: calc(
100dvh - var(--header-height, 0px) - var(
--safe-area-inset-top,
0px
) - var(--safe-area-inset-bottom, 0px)
);
padding-bottom: var(--ha-space-5);
box-sizing: border-box;
overflow-x: hidden;
}
:host(:not([narrow])) ha-card:last-child {
margin-bottom: 24px;
}

View File

@@ -20,9 +20,9 @@ import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon";
import type { ConfigEntry } from "../../../../../data/config_entries";
import { getConfigEntries } from "../../../../../data/config_entries";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../layouts/hass-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
const THREAD_ICON =
"m 17.126982,8.0730792 c 0,-0.7297242 -0.593746,-1.32357 -1.323637,-1.32357 -0.729454,0 -1.323199,0.5938458 -1.323199,1.32357 v 1.3234242 l 1.323199,1.458e-4 c 0.729891,0 1.323637,-0.5937006 1.323637,-1.32357 z M 11.999709,0 C 5.3829818,0 0,5.3838955 0,12.001455 0,18.574352 5.3105455,23.927406 11.865164,24 V 12.012075 l -3.9275642,-2.91e-4 c -1.1669814,0 -2.1169453,0.949979 -2.1169453,2.118323 0,1.16718 0.9499639,2.116868 2.1169453,2.116868 v 2.615717 c -2.6093089,0 -4.732218,-2.12327 -4.732218,-4.732585 0,-2.61048 2.1229091,-4.7343308 4.732218,-4.7343308 l 3.9275642,5.82e-4 v -1.323279 c 0,-2.172296 1.766691,-3.9395777 3.938181,-3.9395777 2.171928,0 3.9392,1.7672817 3.9392,3.9395777 0,2.1721498 -1.767272,3.9395768 -3.9392,3.9395768 l -1.323199,-1.45e-4 V 23.744102 C 19.911127,22.597726 24,17.768833 24,12.001455 24,5.3838955 18.616727,0 11.999709,0 Z";
@@ -312,8 +312,7 @@ export class MatterConfigDashboard extends LitElement {
}
.container {
padding: var(--ha-space-2) var(--ha-space-4)
calc(var(--ha-space-16) + var(--safe-area-inset-bottom, 0px));
padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
}
a[slot="fab"] {

View File

@@ -37,10 +37,10 @@ import {
} from "../../../../../data/zha";
import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow";
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
import { fileDownload } from "../../../../../util/file_download";
import "../../../../../layouts/hass-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import { fileDownload } from "../../../../../util/file_download";
@customElement("zha-config-dashboard")
class ZHAConfigDashboard extends LitElement {
@@ -520,8 +520,7 @@ class ZHAConfigDashboard extends LitElement {
}
.container {
padding: var(--ha-space-2) var(--ha-space-4)
calc(var(--ha-space-20) + var(--safe-area-inset-bottom, 0px));
padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
}
`,
];

View File

@@ -18,7 +18,6 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { goBack } from "../../../../../common/navigate";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-fab";
@@ -29,6 +28,7 @@ import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-progress-ring";
import "../../../../../components/ha-spinner";
import "../../../../../components/ha-svg-icon";
import { goBack } from "../../../../../common/navigate";
import type { ConfigEntry } from "../../../../../data/config_entries";
import {
ERROR_STATES,
@@ -968,8 +968,7 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
}
.container {
padding: var(--ha-space-2) var(--ha-space-4)
calc(var(--ha-space-16) + var(--safe-area-inset-bottom, 0px));
padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
}
`,
];

View File

@@ -86,7 +86,6 @@ class DialogSystemLogDetail extends LitElement {
<ha-dialog
.hass=${this.hass}
.open=${this._open}
width="large"
@closed=${this._dialogClosed}
>
<span slot="headerTitle">${title}</span>

View File

@@ -143,7 +143,8 @@ class HaConfigRepairs extends LitElement {
}
} else if (
issue.domain === "vacuum" &&
issue.translation_key === "segments_changed"
(issue.translation_key === "segments_changed" ||
issue.translation_key === "segments_mapping_not_configured")
) {
const data = await fetchRepairsIssueData(
this.hass.connection,

View File

@@ -1,5 +1,4 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import {
mdiAppleKeyboardCommand,
mdiCog,
@@ -22,15 +21,13 @@ import {
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { UndoRedoController } from "../../../common/controllers/undo-redo-controller";
import { transform } from "../../../common/decorators/transform";
import { fireEvent } from "../../../common/dom/fire_event";
import { goBack, navigate } from "../../../common/navigate";
import { slugify } from "../../../common/string/slugify";
import { promiseTimeout } from "../../../common/util/promise-timeout";
import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
@@ -40,7 +37,6 @@ import "../../../components/ha-svg-icon";
import "../../../components/ha-yaml-editor";
import { substituteBlueprint } from "../../../data/blueprint";
import { validateConfig } from "../../../data/config";
import { fullEntitiesContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import {
type EntityRegistryEntry,
@@ -67,88 +63,47 @@ import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import type { Entries, HomeAssistant, Route } from "../../../types";
import type { Entries } from "../../../types";
import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast";
import { showAutomationModeDialog } from "../automation/automation-mode-dialog/show-dialog-automation-mode";
import type { EntityRegistryUpdate } from "../automation/automation-save-dialog/show-dialog-automation-save";
import { showAutomationSaveDialog } from "../automation/automation-save-dialog/show-dialog-automation-save";
import { showAutomationSaveTimeoutDialog } from "../automation/automation-save-timeout-dialog/show-dialog-automation-save-timeout";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import "./blueprint-script-editor";
import {
AutomationScriptEditorMixin,
automationScriptEditorStyles,
} from "../automation/ha-automation-script-editor-mixin";
import "./manual-script-editor";
import type { HaManualScriptEditor } from "./manual-script-editor";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@customElement("ha-script-editor")
export class HaScriptEditor extends SubscribeMixin(
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
AutomationScriptEditorMixin<ScriptConfig>(
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
)
) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public scriptId: string | null = null;
@property({ attribute: false }) public entityId: string | null = null;
@property({ attribute: false }) public entityRegistry!: EntityRegistryEntry[];
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@state() private _config?: ScriptConfig;
@state() private _dirty = false;
@state() private _errors?: string;
@state() private _yamlErrors?: string;
@state() private _entityId?: string;
@state() private _mode: "gui" | "yaml" = "gui";
@state() private _readOnly = false;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
transformer: function (this: HaScriptEditor, value) {
return value.find(({ entity_id }) => entity_id === this._entityId);
},
watch: ["_entityId"],
})
private _registryEntry?: EntityRegistryEntry;
@query("manual-script-editor")
private _manualEditor?: HaManualScriptEditor;
@state() private _validationErrors?: (string | TemplateResult)[];
@state() private _blueprintConfig?: BlueprintScriptConfig;
@state() private _saving = false;
private _entityRegistryUpdate?: EntityRegistryUpdate;
private _newScriptId?: string;
private _entityRegCreated?: (
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
) => void;
private _undoRedoController = new UndoRedoController<ScriptConfig>(this, {
apply: (config) => this._applyUndoRedo(config),
currentConfig: () => this._config!,
currentConfig: () => this.config!,
});
protected willUpdate(changedProps) {
super.willUpdate(changedProps);
if (
this._entityRegCreated &&
this.entityRegCreated &&
this._newScriptId &&
changedProps.has("entityRegistry")
) {
@@ -157,22 +112,22 @@ export class HaScriptEditor extends SubscribeMixin(
entity.platform === "script" && entity.unique_id === this._newScriptId
);
if (script) {
this._entityRegCreated(script);
this._entityRegCreated = undefined;
this.entityRegCreated(script);
this.entityRegCreated = undefined;
}
}
}
protected render(): TemplateResult | typeof nothing {
if (!this._config) {
return nothing;
if (!this.config) {
return this.renderLoading();
}
const stateObj = this._entityId
? this.hass.states[this._entityId]
const stateObj = this.currentEntityId
? this.hass.states[this.currentEntityId]
: undefined;
const useBlueprint = "use_blueprint" in this._config;
const useBlueprint = "use_blueprint" in this.config;
const shortcutIcon = isMac
? html`<ha-svg-icon .path=${mdiAppleKeyboardCommand}></ha-svg-icon>`
: this.hass.localize("ui.panel.config.automation.editor.ctrl");
@@ -182,11 +137,11 @@ export class HaScriptEditor extends SubscribeMixin(
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.backCallback=${this._backTapped}
.header=${this._config.alias ||
.backCallback=${this.backTapped}
.header=${this.config.alias ||
this.hass.localize("ui.panel.config.script.editor.default_name")}
>
${this._mode === "gui" && !this.narrow
${this.mode === "gui" && !this.narrow
? html`<ha-icon-button
slot="toolbar-icon"
.label=${this.hass.localize("ui.common.undo")}
@@ -252,7 +207,7 @@ export class HaScriptEditor extends SubscribeMixin(
.path=${mdiDotsVertical}
></ha-icon-button>
${this._mode === "gui" && this.narrow
${this.mode === "gui" && this.narrow
? html`<ha-dropdown-item
value="undo"
.disabled=${!this._undoRedoController.canUndo}
@@ -286,7 +241,7 @@ export class HaScriptEditor extends SubscribeMixin(
<ha-dropdown-item .disabled=${!stateObj} value="category">
${this.hass.localize(
`ui.panel.config.scene.picker.${this._registryEntry?.categories?.script ? "edit_category" : "assign_category"}`
`ui.panel.config.scene.picker.${this.registryEntry?.categories?.script ? "edit_category" : "assign_category"}`
)}
<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>
</ha-dropdown-item>
@@ -307,10 +262,10 @@ export class HaScriptEditor extends SubscribeMixin(
></ha-svg-icon>
</ha-dropdown-item>`
: nothing}
${!useBlueprint && !("fields" in this._config)
${!useBlueprint && !("fields" in this.config)
? html`
<ha-dropdown-item
.disabled=${this._readOnly || this._mode === "yaml"}
.disabled=${this.readOnly || this.mode === "yaml"}
value="add_fields"
>
${this.hass.localize(
@@ -326,9 +281,7 @@ export class HaScriptEditor extends SubscribeMixin(
<ha-dropdown-item
value="rename"
.disabled=${!this.scriptId ||
this._readOnly ||
this._mode === "yaml"}
.disabled=${!this.scriptId || this.readOnly || this.mode === "yaml"}
>
${this.hass.localize("ui.panel.config.script.editor.rename")}
<ha-svg-icon slot="icon" .path=${mdiRenameBox}></ha-svg-icon>
@@ -337,7 +290,7 @@ export class HaScriptEditor extends SubscribeMixin(
? html`
<ha-dropdown-item
value="change_mode"
.disabled=${this._readOnly || this._mode === "yaml"}
.disabled=${this.readOnly || this.mode === "yaml"}
>
${this.hass.localize(
"ui.panel.config.script.editor.change_mode"
@@ -351,12 +304,12 @@ export class HaScriptEditor extends SubscribeMixin(
: nothing}
<ha-dropdown-item
.disabled=${!!this._blueprintConfig ||
(!this._readOnly && !this.scriptId)}
.disabled=${!!this.blueprintConfig ||
(!this.readOnly && !this.scriptId)}
value="duplicate"
>
${this.hass.localize(
this._readOnly
this.readOnly
? "ui.panel.config.script.editor.migrate"
: "ui.panel.config.script.editor.duplicate"
)}
@@ -370,7 +323,7 @@ export class HaScriptEditor extends SubscribeMixin(
? html`
<ha-dropdown-item
value="take_control"
.disabled=${this._readOnly}
.disabled=${this.readOnly}
>
${this.hass.localize(
"ui.panel.config.script.editor.take_control"
@@ -382,7 +335,7 @@ export class HaScriptEditor extends SubscribeMixin(
<ha-dropdown-item value="toggle_yaml_mode">
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${this._mode === "gui" ? "yaml" : "ui"}`
`ui.panel.config.automation.editor.edit_${this.mode === "gui" ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-dropdown-item>
@@ -390,7 +343,7 @@ export class HaScriptEditor extends SubscribeMixin(
<wa-divider></wa-divider>
<ha-dropdown-item
.disabled=${this._readOnly || !this.scriptId}
.disabled=${this.readOnly || !this.scriptId}
value="delete"
.variant=${this.scriptId ? "danger" : "default"}
>
@@ -403,8 +356,8 @@ export class HaScriptEditor extends SubscribeMixin(
</ha-svg-icon>
</ha-dropdown-item>
</ha-dropdown>
<div class=${this._mode === "yaml" ? "yaml-mode" : ""}>
${this._mode === "gui"
<div class=${this.mode === "yaml" ? "yaml-mode" : ""}>
${this.mode === "gui"
? html`
<div>
${useBlueprint
@@ -413,10 +366,10 @@ export class HaScriptEditor extends SubscribeMixin(
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
.config=${this._config}
.disabled=${this._readOnly}
.saving=${this._saving}
.dirty=${this._dirty}
.config=${this.config}
.disabled=${this.readOnly}
.saving=${this.saving}
.dirty=${this.dirty}
@value-changed=${this._valueChanged}
@save-script=${this._handleSaveScript}
></blueprint-script-editor>
@@ -426,16 +379,16 @@ export class HaScriptEditor extends SubscribeMixin(
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
.config=${this._config}
.disabled=${this._readOnly}
.dirty=${this._dirty}
.saving=${this._saving}
.config=${this.config}
.disabled=${this.readOnly}
.dirty=${this.dirty}
.saving=${this.saving}
@value-changed=${this._valueChanged}
@editor-save=${this._handleSaveScript}
@save-script=${this._handleSaveScript}
>
<div class="alert-wrapper" slot="alerts">
${this._errors || stateObj?.state === UNAVAILABLE
${this.errors || stateObj?.state === UNAVAILABLE
? html`<ha-alert
alert-type="error"
.title=${stateObj?.state === UNAVAILABLE
@@ -444,7 +397,7 @@ export class HaScriptEditor extends SubscribeMixin(
)
: undefined}
>
${this._errors || this._validationErrors}
${this.errors || this.validationErrors}
${stateObj?.state === UNAVAILABLE
? html`<ha-svg-icon
slot="icon"
@@ -453,7 +406,7 @@ export class HaScriptEditor extends SubscribeMixin(
: nothing}
</ha-alert>`
: nothing}
${this._blueprintConfig
${this.blueprintConfig
? html`<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.script.editor.confirm_take_control"
@@ -461,21 +414,21 @@ export class HaScriptEditor extends SubscribeMixin(
<div slot="action" style="display: flex;">
<ha-button
appearance="plain"
@click=${this._takeControlSave}
@click=${this.takeControlSave}
>${this.hass.localize(
"ui.common.yes"
)}</ha-button
>
<ha-button
appearance="plain"
@click=${this._revertBlueprint}
@click=${this.revertBlueprint}
>${this.hass.localize(
"ui.common.no"
)}</ha-button
>
</div>
</ha-alert>`
: this._readOnly
: this.readOnly
? html`<ha-alert
alert-type="warning"
dismissable
@@ -498,11 +451,11 @@ export class HaScriptEditor extends SubscribeMixin(
`}
</div>
`
: this._mode === "yaml"
: this.mode === "yaml"
? html`<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._preprocessYaml()}
.readOnly=${this._readOnly}
.readOnly=${this.readOnly}
disable-fullscreen
@value-changed=${this._yamlChanged}
@editor-save=${this._handleSaveScript}
@@ -510,9 +463,9 @@ export class HaScriptEditor extends SubscribeMixin(
></ha-yaml-editor>
<ha-fab
slot="fab"
class=${!this._readOnly && this._dirty ? "dirty" : ""}
class=${!this.readOnly && this.dirty ? "dirty" : ""}
.label=${this.hass.localize("ui.common.save")}
.disabled=${this._saving}
.disabled=${this.saving}
extended
@click=${this._handleSaveScript}
>
@@ -551,26 +504,26 @@ export class HaScriptEditor extends SubscribeMixin(
const entity = this.entityRegistry.find(
(ent) => ent.platform === "script" && ent.unique_id === this.scriptId
);
this._entityId = entity?.entity_id;
this.currentEntityId = entity?.entity_id;
}
if (changedProps.has("scriptId") && !this.scriptId && this.hass) {
const initData = getScriptEditorInitData();
this._dirty = !!initData;
this.dirty = !!initData;
const baseConfig: Partial<ScriptConfig> = {};
if (!initData || !("use_blueprint" in initData)) {
baseConfig.sequence = [];
}
this._config = {
this.config = {
...baseConfig,
...initData,
} as ScriptConfig;
this._readOnly = false;
this.readOnly = false;
}
if (changedProps.has("entityId") && this.entityId) {
getScriptStateConfig(this.hass, this.entityId).then((c) => {
this._config = normalizeScriptConfig(c.config);
this.config = normalizeScriptConfig(c.config);
this._checkValidation();
});
const regEntry = this.entityRegistry.find(
@@ -579,25 +532,25 @@ export class HaScriptEditor extends SubscribeMixin(
if (regEntry?.unique_id) {
this.scriptId = regEntry.unique_id;
}
this._entityId = this.entityId;
this._dirty = false;
this._readOnly = true;
this.currentEntityId = this.entityId;
this.dirty = false;
this.readOnly = true;
}
}
private async _checkValidation() {
this._validationErrors = undefined;
if (!this._entityId || !this._config) {
this.validationErrors = undefined;
if (!this.currentEntityId || !this.config) {
return;
}
const stateObj = this.hass.states[this._entityId];
const stateObj = this.hass.states[this.currentEntityId];
if (stateObj?.state !== UNAVAILABLE) {
return;
}
const validation = await validateConfig(this.hass, {
actions: this._config.sequence,
actions: this.config.sequence,
});
this._validationErrors = (
this.validationErrors = (
Object.entries(validation) as Entries<typeof validation>
).map(([key, value]) =>
value.valid
@@ -612,13 +565,13 @@ export class HaScriptEditor extends SubscribeMixin(
private async _loadConfig() {
fetchScriptFileConfig(this.hass, this.scriptId!).then(
(config) => {
this._dirty = false;
this._readOnly = false;
this._config = normalizeScriptConfig(config);
this.dirty = false;
this.readOnly = false;
this.config = normalizeScriptConfig(config);
const entity = this.entityRegistry.find(
(ent) => ent.platform === "script" && ent.unique_id === this.scriptId
);
this._entityId = entity?.entity_id;
this.currentEntityId = entity?.entity_id;
this._checkValidation();
},
(resp) => {
@@ -647,19 +600,19 @@ export class HaScriptEditor extends SubscribeMixin(
}
private _valueChanged(ev) {
if (this._config) {
this._undoRedoController.commit(this._config);
if (this.config) {
this._undoRedoController.commit(this.config);
}
this._config = ev.detail.value;
this._errors = undefined;
this._dirty = true;
this.config = ev.detail.value;
this.errors = undefined;
this.dirty = true;
}
private async _runScript() {
if (hasScriptFields(this.hass, this._entityId!)) {
if (hasScriptFields(this.hass, this.currentEntityId!)) {
showMoreInfoDialog(this, {
entityId: this._entityId!,
entityId: this.currentEntityId!,
});
return;
}
@@ -667,20 +620,13 @@ export class HaScriptEditor extends SubscribeMixin(
await triggerScript(this.hass, this.scriptId!);
showToast(this, {
message: this.hass.localize("ui.notification_toast.triggered", {
name: this._config!.alias,
name: this.config!.alias,
}),
});
}
private _showSettings() {
showMoreInfoDialog(this, {
entityId: this._entityId!,
view: "settings",
});
}
private _editCategory() {
if (!this._registryEntry) {
if (!this.registryEntry) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.scene.picker.no_category_support"
@@ -693,7 +639,7 @@ export class HaScriptEditor extends SubscribeMixin(
}
showAssignCategoryDialog(this, {
scope: "script",
entityReg: this._registryEntry,
entityReg: this.registryEntry,
});
}
@@ -730,7 +676,7 @@ export class HaScriptEditor extends SubscribeMixin(
private async _showTrace() {
if (this.scriptId) {
const result = await this._confirmUnsavedChanged();
const result = await this.confirmUnsavedChanged();
if (result) {
navigate(`/config/script/trace/${this.scriptId}`);
}
@@ -738,47 +684,47 @@ export class HaScriptEditor extends SubscribeMixin(
}
private _addFields() {
if ("fields" in this._config!) {
if ("fields" in this.config!) {
return;
}
if (this._config) {
this._undoRedoController.commit(this._config);
if (this.config) {
this._undoRedoController.commit(this.config);
}
this._manualEditor?.addFields();
this._dirty = true;
this.dirty = true;
}
private _preprocessYaml() {
return this._config;
return this.config;
}
private _yamlChanged(ev: CustomEvent) {
ev.stopPropagation();
this._dirty = true;
this.dirty = true;
if (!ev.detail.isValid) {
this._yamlErrors = ev.detail.errorMsg;
this.yamlErrors = ev.detail.errorMsg;
return;
}
this._yamlErrors = undefined;
this._config = ev.detail.value;
this._errors = undefined;
this.yamlErrors = undefined;
this.config = ev.detail.value;
this.errors = undefined;
}
private async _confirmUnsavedChanged(): Promise<boolean> {
if (!this._dirty) {
protected async confirmUnsavedChanged(): Promise<boolean> {
if (!this.dirty) {
return true;
}
return new Promise<boolean>((resolve) => {
showAutomationSaveDialog(this, {
config: this._config!,
config: this.config!,
domain: "script",
updateConfig: async (config, entityRegistryUpdate) => {
this._config = config;
this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true;
this.config = config;
this.entityRegistryUpdate = entityRegistryUpdate;
this.dirty = true;
this.requestUpdate();
const id = this.scriptId || String(Date.now());
@@ -794,8 +740,8 @@ export class HaScriptEditor extends SubscribeMixin(
},
onClose: () => resolve(false),
onDiscard: () => resolve(true),
entityRegistryUpdate: this._entityRegistryUpdate,
entityRegistryEntry: this._registryEntry,
entityRegistryUpdate: this.entityRegistryUpdate,
entityRegistryEntry: this.registryEntry,
title: this.hass.localize(
this.scriptId
? "ui.panel.config.script.editor.leave.unsaved_confirm_title"
@@ -811,15 +757,8 @@ export class HaScriptEditor extends SubscribeMixin(
});
}
private _backTapped = async () => {
const result = await this._confirmUnsavedChanged();
if (result) {
afterNextRender(() => goBack("/config"));
}
};
private async _takeControl() {
const config = this._config as BlueprintScriptConfig;
const config = this.config as BlueprintScriptConfig;
try {
const result = await substituteBlueprint(
@@ -835,35 +774,20 @@ export class HaScriptEditor extends SubscribeMixin(
description: config.description,
};
this._blueprintConfig = config;
this._config = newConfig;
if (this._mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
this.blueprintConfig = config;
this.config = newConfig;
if (this.mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this.config);
}
this._readOnly = true;
this._errors = undefined;
this.readOnly = true;
this.errors = undefined;
} catch (err: any) {
this._errors = err.message;
this.errors = err.message;
}
}
private _revertBlueprint() {
this._config = this._blueprintConfig;
if (this._mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
}
this._blueprintConfig = undefined;
this._readOnly = false;
}
private _takeControlSave() {
this._readOnly = false;
this._dirty = true;
this._blueprintConfig = undefined;
}
private async _duplicate() {
const result = this._readOnly
const result = this.readOnly
? await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.script.picker.migrate_script"
@@ -872,14 +796,14 @@ export class HaScriptEditor extends SubscribeMixin(
"ui.panel.config.script.picker.migrate_script_description"
),
})
: await this._confirmUnsavedChanged();
: await this.confirmUnsavedChanged();
if (result) {
this._entityId = undefined;
this.currentEntityId = undefined;
showScriptEditor({
...this._config,
alias: this._readOnly
? this._config?.alias
: `${this._config?.alias} (${this.hass.localize(
...this.config,
alias: this.readOnly
? this.config?.alias
: `${this.config?.alias} (${this.hass.localize(
"ui.panel.config.script.picker.duplicate"
)})`,
});
@@ -893,7 +817,7 @@ export class HaScriptEditor extends SubscribeMixin(
),
text: this.hass.localize(
"ui.panel.config.script.editor.delete_confirm_text",
{ name: this._config?.alias }
{ name: this.config?.alias }
),
confirmText: this.hass!.localize("ui.common.delete"),
destructive: true,
@@ -907,42 +831,20 @@ export class HaScriptEditor extends SubscribeMixin(
goBack("/config");
}
private async _switchUiMode() {
if (this._yamlErrors) {
const result = await showConfirmationDialog(this, {
text: html`${this.hass.localize(
"ui.panel.config.automation.editor.switch_ui_yaml_error"
)}<br /><br />${this._yamlErrors}`,
confirmText: this.hass!.localize("ui.common.continue"),
destructive: true,
dismissText: this.hass!.localize("ui.common.cancel"),
});
if (!result) {
return;
}
}
this._yamlErrors = undefined;
this._mode = "gui";
}
private _switchYamlMode() {
this._mode = "yaml";
}
private async _promptScriptAlias(): Promise<boolean> {
return new Promise((resolve) => {
showAutomationSaveDialog(this, {
config: this._config!,
config: this.config!,
domain: "script",
updateConfig: async (config, entityRegistryUpdate) => {
this._config = config;
this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true;
this.config = config;
this.entityRegistryUpdate = entityRegistryUpdate;
this.dirty = true;
this.requestUpdate();
resolve(true);
},
onClose: () => resolve(false),
entityRegistryUpdate: this._entityRegistryUpdate,
entityRegistryUpdate: this.entityRegistryUpdate,
entityRegistryEntry: this.entityRegistry.find(
(entry) => entry.unique_id === this.scriptId
),
@@ -953,10 +855,10 @@ export class HaScriptEditor extends SubscribeMixin(
private async _promptScriptMode(): Promise<void> {
return new Promise((resolve) => {
showAutomationModeDialog(this, {
config: this._config!,
config: this.config!,
updateConfig: (config) => {
this._config = config;
this._dirty = true;
this.config = config;
this.dirty = true;
this.requestUpdate();
resolve();
},
@@ -966,9 +868,9 @@ export class HaScriptEditor extends SubscribeMixin(
}
private async _handleSaveScript() {
if (this._yamlErrors) {
if (this.yamlErrors) {
showToast(this, {
message: this._yamlErrors,
message: this.yamlErrors,
});
return;
}
@@ -980,9 +882,9 @@ export class HaScriptEditor extends SubscribeMixin(
if (!saved) {
return;
}
this._entityId = this._computeEntityIdFromAlias(this._config!.alias);
this.currentEntityId = this._computeEntityIdFromAlias(this.config!.alias);
}
const id = this.scriptId || this._entityId || Date.now();
const id = this.scriptId || this.currentEntityId || Date.now();
await this._saveScript(id);
if (!this.scriptId) {
@@ -991,13 +893,13 @@ export class HaScriptEditor extends SubscribeMixin(
}
private async _saveScript(id): Promise<void> {
this._saving = true;
this.saving = true;
let entityRegPromise: Promise<EntityRegistryEntry> | undefined;
if (this._entityRegistryUpdate !== undefined && !this.scriptId) {
if (this.entityRegistryUpdate !== undefined && !this.scriptId) {
this._newScriptId = id.toString();
entityRegPromise = new Promise<EntityRegistryEntry>((resolve) => {
this._entityRegCreated = resolve;
this.entityRegCreated = resolve;
});
}
@@ -1005,11 +907,11 @@ export class HaScriptEditor extends SubscribeMixin(
await this.hass!.callApi(
"POST",
"config/script/config/" + id,
this._config
this.config
);
if (this._entityRegistryUpdate !== undefined) {
let entityId = this._entityId;
if (this.entityRegistryUpdate !== undefined) {
let entityId = this.currentEntityId;
// wait for new script to appear in entity registry
if (entityRegPromise) {
@@ -1044,23 +946,23 @@ export class HaScriptEditor extends SubscribeMixin(
if (entityId) {
await updateEntityRegistryEntry(this.hass, entityId, {
categories: {
script: this._entityRegistryUpdate.category || null,
script: this.entityRegistryUpdate.category || null,
},
labels: this._entityRegistryUpdate.labels || [],
area_id: this._entityRegistryUpdate.area || null,
labels: this.entityRegistryUpdate.labels || [],
area_id: this.entityRegistryUpdate.area || null,
});
}
}
this._dirty = false;
this.dirty = false;
} catch (errors: any) {
this._errors = errors.body?.message || errors.error || errors.body;
this.errors = errors.body?.message || errors.error || errors.body;
showToast(this, {
message: errors.body?.message || errors.error || errors.body,
});
throw errors;
} finally {
this._saving = false;
this.saving = false;
}
}
@@ -1077,14 +979,6 @@ export class HaScriptEditor extends SubscribeMixin(
};
}
protected get isDirty() {
return this._dirty;
}
protected async promptDiscardChanges() {
return this._confirmUnsavedChanged();
}
// @ts-ignore
private _collapseAll() {
this._manualEditor?.collapseAll();
@@ -1109,8 +1003,8 @@ export class HaScriptEditor extends SubscribeMixin(
private _applyUndoRedo(config: ScriptConfig) {
this._manualEditor?.triggerCloseSidebar();
this._config = config;
this._dirty = true;
this.config = config;
this.dirty = true;
}
private _undo() {
@@ -1139,7 +1033,7 @@ export class HaScriptEditor extends SubscribeMixin(
this._showInfo();
break;
case "settings":
this._showSettings();
this.showSettings();
break;
case "category":
this._editCategory();
@@ -1163,11 +1057,11 @@ export class HaScriptEditor extends SubscribeMixin(
this._takeControl();
break;
case "toggle_yaml_mode":
if (this._mode === "gui") {
this._switchYamlMode();
if (this.mode === "gui") {
this.switchYamlMode();
break;
}
this._switchUiMode();
this.switchUiMode();
break;
case "delete":
this._deleteConfirm();
@@ -1181,19 +1075,8 @@ export class HaScriptEditor extends SubscribeMixin(
static get styles(): CSSResultGroup {
return [
haStyle,
automationScriptEditorStyles,
css`
:host {
--ha-automation-editor-max-width: var(
--ha-automation-editor-width,
1540px
);
}
.yaml-mode {
height: 100%;
display: flex;
flex-direction: column;
padding-bottom: 0;
}
manual-script-editor,
blueprint-script-editor {
margin: 0 auto;
@@ -1244,29 +1127,9 @@ export class HaScriptEditor extends SubscribeMixin(
padding: 0 12px;
}
ha-yaml-editor {
flex-grow: 1;
--actions-border-radius: var(--ha-border-radius-square);
--code-mirror-height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
}
p {
margin-bottom: 0;
}
span[slot="introduction"] a {
color: var(--primary-color);
}
ha-fab {
position: fixed;
right: 16px;
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
}
ha-fab.dirty {
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
}
.header {
display: flex;
margin: 16px 0;
@@ -1280,15 +1143,6 @@ export class HaScriptEditor extends SubscribeMixin(
.header a {
color: var(--secondary-text-color);
}
ha-tooltip ha-svg-icon {
width: 12px;
}
ha-tooltip .shortcut {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 2px;
}
`,
];
}

View File

@@ -670,10 +670,10 @@ export class AssistPipelineDebug extends LitElement {
background-color: var(--light-primary-color);
color: var(--text-light-primary-color, var(--primary-text-color));
direction: var(--direction);
}
.tool_result [slot="header"] {
color: var(--text-light-primary-color, var(--primary-text-color));
--primary-text-color: var(
--text-light-primary-color,
var(--primary-text-color)
);
}
.message.user,

View File

@@ -7,7 +7,11 @@ import type { HomeAssistant } from "../../../types";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../ha-panel-energy";
import { shouldShowFloorsAndAreas } from "./show-floors-and-areas";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
import {
LARGE_SCREEN_CONDITION,
SMALL_SCREEN_CONDITION,
} from "../../lovelace/strategies/helpers/screen-conditions";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
@customElement("power-view-strategy")
export class PowerViewStrategy extends ReactiveElement {
@@ -45,15 +49,22 @@ export class PowerViewStrategy extends ReactiveElement {
(source) => source.type === "gas" && source.stat_rate
);
const tileSection: LovelaceSectionConfig = {
type: "grid",
cards: [],
column_span: 2,
};
const chartsSection: LovelaceSectionConfig = {
type: "grid",
cards: [],
column_span: 2,
};
const badges: LovelaceBadgeConfig[] = [];
const tiles: LovelaceCardConfig[] = [];
const view: LovelaceViewConfig = {
type: "sections",
sections: [chartsSection],
sections: [tileSection, chartsSection],
max_columns: 2,
};
// No sources configured
@@ -69,10 +80,11 @@ export class PowerViewStrategy extends ReactiveElement {
}
if (hasPowerSources) {
badges.push({
const card = {
type: "power-total",
collection_key: collectionKey,
});
};
tiles.push(card);
chartsSection.cards!.push({
title: hass.localize("ui.panel.energy.cards.power_sources_graph_title"),
@@ -85,17 +97,19 @@ export class PowerViewStrategy extends ReactiveElement {
}
if (hasGasSources) {
badges.push({
const card = {
type: "gas-total",
collection_key: collectionKey,
});
};
tiles.push({ ...card });
}
if (hasWaterSources) {
badges.push({
const card = {
type: "water-total",
collection_key: collectionKey,
});
};
tiles.push({ ...card });
}
if (hasPowerDevices) {
@@ -134,8 +148,21 @@ export class PowerViewStrategy extends ReactiveElement {
});
}
if (badges.length) {
view.badges = badges;
tiles.forEach((card) => {
tileSection.cards!.push({
...card,
grid_options: { columns: 24 / tiles.length },
});
});
if (tiles.length > 2) {
// On small screens with 3 tiles, show them in 1 column
tileSection.visibility = [LARGE_SCREEN_CONDITION];
view.sections!.unshift({
type: "grid",
cards: tiles,
visibility: [SMALL_SCREEN_CONDITION],
});
}
return view;

View File

@@ -48,20 +48,3 @@ export interface EntityBadgeConfig extends LovelaceBadgeConfig {
*/
display_type?: DisplayType;
}
interface EnergyTotalBadgeConfig extends LovelaceBadgeConfig {
title?: string;
collection_key?: string;
}
export interface PowerTotalBadgeConfig extends EnergyTotalBadgeConfig {
type: "power-total";
}
export interface WaterTotalBadgeConfig extends EnergyTotalBadgeConfig {
type: "water-total";
}
export interface GasTotalBadgeConfig extends EnergyTotalBadgeConfig {
type: "gas-total";
}

View File

@@ -49,14 +49,6 @@ class HuiClimateFanModesCardFeature
@state() _currentFanMode?: string;
private _renderFanModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this._stateObj}
attribute="fan_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -183,8 +175,14 @@ class HuiClimateFanModesCardFeature
.value=${this._currentFanMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
@wa-select=${this._valueChanged}
.options=${options}
.renderIcon=${this._renderFanModeIcon}
.options=${options.map((option) => ({
...option,
attributeIcon: {
stateObj: stateObj,
attribute: "fan_mode",
attributeValue: option.value,
},
}))}
><ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
</ha-control-select-menu>
`;

View File

@@ -48,14 +48,6 @@ class HuiClimatePresetModesCardFeature
@state() _currentPresetMode?: string;
private _renderPresetModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this._stateObj}
attribute="preset_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -187,8 +179,14 @@ class HuiClimatePresetModesCardFeature
.value=${this._currentPresetMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
@wa-select=${this._valueChanged}
.options=${options}
.renderIcon=${this._renderPresetModeIcon}
.options=${options.map((option) => ({
...option,
attributeIcon: {
stateObj: stateObj,
attribute: "preset_mode",
attributeValue: option.value,
},
}))}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
</ha-control-select-menu>

View File

@@ -48,14 +48,6 @@ class HuiClimateSwingHorizontalModesCardFeature
@state() _currentSwingHorizontalMode?: string;
private _renderSwingHorizontalModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this._stateObj}
attribute="swing_horizontal_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -195,8 +187,14 @@ class HuiClimateSwingHorizontalModesCardFeature
.value=${this._currentSwingHorizontalMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
@wa-select=${this._valueChanged}
.options=${options}
.renderIcon=${this._renderSwingHorizontalModeIcon}
.options=${options.map((option) => ({
...option,
attributeIcon: {
stateObj: stateObj,
attribute: "swing_horizontal_mode",
attributeValue: option.value,
},
}))}
>
<ha-svg-icon slot="icon" .path=${mdiArrowOscillating}></ha-svg-icon>
</ha-control-select-menu>

View File

@@ -48,14 +48,6 @@ class HuiClimateSwingModesCardFeature
@state() _currentSwingMode?: string;
private _renderSwingModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this._stateObj}
attribute="swing_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -187,8 +179,14 @@ class HuiClimateSwingModesCardFeature
.value=${this._currentSwingMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
@wa-select=${this._valueChanged}
.options=${options}
.renderIcon=${this._renderSwingModeIcon}
.options=${options.map((option) => ({
...option,
attributeIcon: {
stateObj,
attribute: "swing_mode",
attributeValue: option.value,
},
}))}
><ha-svg-icon slot="icon" .path=${mdiArrowOscillating}></ha-svg-icon>
</ha-control-select-menu>
`;

View File

@@ -47,14 +47,6 @@ class HuiFanPresetModesCardFeature
@state() _currentPresetMode?: string;
private _renderPresetModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this._stateObj}
attribute="preset_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -181,8 +173,14 @@ class HuiFanPresetModesCardFeature
.value=${this._currentPresetMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
@wa-select=${this._valueChanged}
.options=${options}
.renderIcon=${this._renderPresetModeIcon}
.options=${options.map((option) => ({
...option,
attributeIcon: {
stateObj: stateObj,
attribute: "preset_mode",
attributeValue: option.value,
},
}))}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
</ha-control-select-menu>

View File

@@ -48,14 +48,6 @@ class HuiHumidifierModesCardFeature
@state() _currentMode?: string;
private _renderModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this._stateObj}
attribute="mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -182,8 +174,14 @@ class HuiHumidifierModesCardFeature
.value=${this._currentMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
@wa-select=${this._valueChanged}
.options=${options}
.renderIcon=${this._renderModeIcon}
.options=${options.map((option) => ({
...option,
attributeIcon: {
stateObj,
attribute: "mode",
attributeValue: option.value,
},
}))}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
</ha-control-select-menu>

View File

@@ -49,14 +49,6 @@ class HuiWaterHeaterOperationModeCardFeature
@state() _currentOperationMode?: OperationMode;
private _renderOperationModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this._stateObj}
attribute="operation_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -161,8 +153,14 @@ class HuiWaterHeaterOperationModeCardFeature
.value=${this._currentOperationMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
@wa-select=${this._valueChanged}
.options=${options}
.renderIcon=${this._renderOperationModeIcon}
.options=${options.map((option) => ({
...option,
attributeIcon: {
stateObj: this._stateObj,
attribute: "operation_mode",
attributeValue: option.value,
},
}))}
>
<ha-svg-icon slot="icon" .path=${mdiWaterBoiler}></ha-svg-icon>
</ha-control-select-menu>

View File

@@ -218,9 +218,7 @@ function formatTooltip(
}
// when comparing the first value is offset to match the main period
// and the real date is in the third value
// find the first param with the real date to handle gap-filled entries
const origDate = params.find((p) => p.value?.[2] != null)?.value?.[2];
const date = new Date(origDate ?? params[0].value?.[0]);
const date = new Date(params[0].value?.[2] ?? params[0].value?.[0]);
let period: string;
if (suggestedPeriod === "month") {

View File

@@ -3,8 +3,11 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../components/ha-badge";
import "../../../../components/ha-card";
import "../../../../components/ha-svg-icon";
import "../../../../components/tile/ha-tile-container";
import "../../../../components/tile/ha-tile-icon";
import "../../../../components/tile/ha-tile-info";
import type { EnergyData, EnergyPreferences } from "../../../../data/energy";
import {
formatFlowRateShort,
@@ -13,17 +16,18 @@ import {
} from "../../../../data/energy";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../../types";
import type { LovelaceBadge } from "../../types";
import type { GasTotalBadgeConfig } from "../types";
import type { LovelaceCard, LovelaceGridOptions } from "../../types";
import { tileCardStyle } from "../tile/tile-card-style";
import type { GasTotalCardConfig } from "../types";
@customElement("hui-gas-total-badge")
export class HuiGasTotalBadge
@customElement("hui-gas-total-card")
export class HuiGasTotalCard
extends SubscribeMixin(LitElement)
implements LovelaceBadge
implements LovelaceCard
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config?: GasTotalBadgeConfig;
@state() private _config?: GasTotalCardConfig;
@state() private _data?: EnergyData;
@@ -31,7 +35,7 @@ export class HuiGasTotalBadge
protected hassSubscribeRequiredHostProps = ["_config"];
public setConfig(config: GasTotalBadgeConfig): void {
public setConfig(config: GasTotalCardConfig): void {
this._config = config;
}
@@ -45,19 +49,34 @@ export class HuiGasTotalBadge
];
}
public getCardSize(): Promise<number> | number {
return 1;
}
getGridOptions(): LovelaceGridOptions {
return {
columns: 12,
min_columns: 6,
rows: 1,
min_rows: 1,
};
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.has("_config") || changedProps.has("_data")) {
return true;
}
// Check if any of the tracked entity states have changed
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || !this._entities.size) {
return true;
}
// Only update if one of our tracked entities changed
for (const entityId of this._entities) {
if (oldHass.states[entityId] !== this.hass?.states[entityId]) {
if (oldHass.states[entityId] !== this.hass.states[entityId]) {
return true;
}
}
@@ -103,22 +122,32 @@ export class HuiGasTotalBadge
this.hass.localize("ui.panel.lovelace.cards.energy.gas_total_title");
return html`
<ha-badge .label=${name}>
<ha-svg-icon slot="icon" .path=${mdiFire}></ha-svg-icon>
${displayValue}
</ha-badge>
<ha-card>
<ha-tile-container .interactive=${false}>
<ha-tile-icon slot="icon" data-domain="sensor" data-state="active">
<ha-svg-icon slot="icon" .path=${mdiFire}></ha-svg-icon>
</ha-tile-icon>
<ha-tile-info slot="info">
<span slot="primary" class="primary">${name}</span>
<span slot="secondary" class="secondary">${displayValue}</span>
</ha-tile-info>
</ha-tile-container>
</ha-card>
`;
}
static styles = css`
ha-badge {
--badge-color: var(--energy-gas-color);
}
`;
static styles = [
tileCardStyle,
css`
:host {
--tile-color: var(--energy-gas-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"hui-gas-total-badge": HuiGasTotalBadge;
"hui-gas-total-card": HuiGasTotalCard;
}
}

View File

@@ -4,8 +4,11 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/ha-badge";
import "../../../../components/ha-card";
import "../../../../components/ha-svg-icon";
import "../../../../components/tile/ha-tile-container";
import "../../../../components/tile/ha-tile-icon";
import "../../../../components/tile/ha-tile-info";
import type { EnergyData, EnergyPreferences } from "../../../../data/energy";
import {
getEnergyDataCollection,
@@ -13,17 +16,18 @@ import {
} from "../../../../data/energy";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../../types";
import type { LovelaceBadge } from "../../types";
import type { PowerTotalBadgeConfig } from "../types";
import type { LovelaceCard, LovelaceGridOptions } from "../../types";
import { tileCardStyle } from "../tile/tile-card-style";
import type { PowerTotalCardConfig } from "../types";
@customElement("hui-power-total-badge")
export class HuiPowerTotalBadge
@customElement("hui-power-total-card")
export class HuiPowerTotalCard
extends SubscribeMixin(LitElement)
implements LovelaceBadge
implements LovelaceCard
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config?: PowerTotalBadgeConfig;
@state() private _config?: PowerTotalCardConfig;
@state() private _data?: EnergyData;
@@ -31,7 +35,7 @@ export class HuiPowerTotalBadge
protected hassSubscribeRequiredHostProps = ["_config"];
public setConfig(config: PowerTotalBadgeConfig): void {
public setConfig(config: PowerTotalCardConfig): void {
this._config = config;
}
@@ -45,19 +49,34 @@ export class HuiPowerTotalBadge
];
}
public getCardSize(): Promise<number> | number {
return 1;
}
getGridOptions(): LovelaceGridOptions {
return {
columns: 12,
min_columns: 6,
rows: 1,
min_rows: 1,
};
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.has("_config") || changedProps.has("_data")) {
return true;
}
// Check if any of the tracked entity states have changed
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || !this._entities.size) {
return true;
}
// Only update if one of our tracked entities changed
for (const entityId of this._entities) {
if (oldHass.states[entityId] !== this.hass?.states[entityId]) {
if (oldHass.states[entityId] !== this.hass.states[entityId]) {
return true;
}
}
@@ -75,10 +94,10 @@ export class HuiPowerTotalBadge
this._entities.clear();
let solar = 0;
let fromGrid = 0;
let toGrid = 0;
let fromBattery = 0;
let toBattery = 0;
let from_grid = 0;
let to_grid = 0;
let from_battery = 0;
let to_battery = 0;
prefs.energy_sources.forEach((source) => {
if (source.type === "solar" && source.stat_rate) {
@@ -86,17 +105,17 @@ export class HuiPowerTotalBadge
if (value > 0) solar += value;
} else if (source.type === "grid" && source.stat_rate) {
const value = this._getCurrentPower(source.stat_rate);
if (value > 0) fromGrid += value;
else if (value < 0) toGrid += Math.abs(value);
if (value > 0) from_grid += value;
else if (value < 0) to_grid += Math.abs(value);
} else if (source.type === "battery" && source.stat_rate) {
const value = this._getCurrentPower(source.stat_rate);
if (value > 0) fromBattery += value;
else if (value < 0) toBattery += Math.abs(value);
if (value > 0) from_battery += value;
else if (value < 0) to_battery += Math.abs(value);
}
});
const usedTotal = fromGrid + solar + fromBattery - toGrid - toBattery;
return Math.max(0, usedTotal);
const used_total = from_grid + solar + from_battery - to_grid - to_battery;
return Math.max(0, used_total);
}
protected render() {
@@ -122,22 +141,35 @@ export class HuiPowerTotalBadge
this.hass.localize("ui.panel.lovelace.cards.energy.power_total_title");
return html`
<ha-badge .label=${name}>
<ha-svg-icon slot="icon" .path=${mdiHomeLightningBolt}></ha-svg-icon>
${displayValue}
</ha-badge>
<ha-card>
<ha-tile-container .interactive=${false}>
<ha-tile-icon slot="icon" data-domain="sensor" data-state="active">
<ha-svg-icon
slot="icon"
.path=${mdiHomeLightningBolt}
></ha-svg-icon>
</ha-tile-icon>
<ha-tile-info slot="info">
<span slot="primary" class="primary">${name}</span>
<span slot="secondary" class="secondary">${displayValue}</span>
</ha-tile-info>
</ha-tile-container>
</ha-card>
`;
}
static styles = css`
ha-badge {
--badge-color: var(--primary-color);
}
`;
static styles = [
tileCardStyle,
css`
:host {
--tile-color: var(--primary-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"hui-power-total-badge": HuiPowerTotalBadge;
"hui-power-total-card": HuiPowerTotalCard;
}
}

View File

@@ -3,8 +3,11 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../components/ha-badge";
import "../../../../components/ha-card";
import "../../../../components/ha-svg-icon";
import "../../../../components/tile/ha-tile-container";
import "../../../../components/tile/ha-tile-icon";
import "../../../../components/tile/ha-tile-info";
import type { EnergyData, EnergyPreferences } from "../../../../data/energy";
import {
formatFlowRateShort,
@@ -13,17 +16,18 @@ import {
} from "../../../../data/energy";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../../types";
import type { LovelaceBadge } from "../../types";
import type { WaterTotalBadgeConfig } from "../types";
import type { LovelaceCard, LovelaceGridOptions } from "../../types";
import { tileCardStyle } from "../tile/tile-card-style";
import type { WaterTotalCardConfig } from "../types";
@customElement("hui-water-total-badge")
export class HuiWaterTotalBadge
@customElement("hui-water-total-card")
export class HuiWaterTotalCard
extends SubscribeMixin(LitElement)
implements LovelaceBadge
implements LovelaceCard
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config?: WaterTotalBadgeConfig;
@state() private _config?: WaterTotalCardConfig;
@state() private _data?: EnergyData;
@@ -31,7 +35,7 @@ export class HuiWaterTotalBadge
protected hassSubscribeRequiredHostProps = ["_config"];
public setConfig(config: WaterTotalBadgeConfig): void {
public setConfig(config: WaterTotalCardConfig): void {
this._config = config;
}
@@ -45,19 +49,34 @@ export class HuiWaterTotalBadge
];
}
public getCardSize(): Promise<number> | number {
return 1;
}
getGridOptions(): LovelaceGridOptions {
return {
columns: 12,
min_columns: 6,
rows: 1,
min_rows: 1,
};
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.has("_config") || changedProps.has("_data")) {
return true;
}
// Check if any of the tracked entity states have changed
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || !this._entities.size) {
return true;
}
// Only update if one of our tracked entities changed
for (const entityId of this._entities) {
if (oldHass.states[entityId] !== this.hass?.states[entityId]) {
if (oldHass.states[entityId] !== this.hass.states[entityId]) {
return true;
}
}
@@ -103,22 +122,32 @@ export class HuiWaterTotalBadge
this.hass.localize("ui.panel.lovelace.cards.energy.water_total_title");
return html`
<ha-badge .label=${name}>
<ha-svg-icon slot="icon" .path=${mdiWater}></ha-svg-icon>
${displayValue}
</ha-badge>
<ha-card>
<ha-tile-container .interactive=${false}>
<ha-tile-icon slot="icon" data-domain="sensor" data-state="active">
<ha-svg-icon slot="icon" .path=${mdiWater}></ha-svg-icon>
</ha-tile-icon>
<ha-tile-info slot="info">
<span slot="primary" class="primary">${name}</span>
<span slot="secondary" class="secondary">${displayValue}</span>
</ha-tile-info>
</ha-tile-container>
</ha-card>
`;
}
static styles = css`
ha-badge {
--badge-color: var(--energy-water-color);
}
`;
static styles = [
tileCardStyle,
css`
:host {
--tile-color: var(--energy-water-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"hui-water-total-badge": HuiWaterTotalBadge;
"hui-water-total-card": HuiWaterTotalCard;
}
}

View File

@@ -265,6 +265,21 @@ export interface PowerSourcesGraphCardConfig extends EnergyCardBaseConfig {
show_legend?: boolean;
}
export interface PowerTotalCardConfig extends EnergyCardBaseConfig {
type: "power-total";
title?: string;
}
export interface WaterTotalCardConfig extends EnergyCardBaseConfig {
type: "water-total";
title?: string;
}
export interface GasTotalCardConfig extends EnergyCardBaseConfig {
type: "gas-total";
title?: string;
}
export interface PowerSankeyCardConfig extends EnergyCardBaseConfig {
type: "power-sankey";
title?: string;

View File

@@ -10,9 +10,6 @@ const ALWAYS_LOADED_TYPES = new Set(["error", "entity"]);
const LAZY_LOAD_TYPES = {
"entity-filter": () => import("../badges/hui-entity-filter-badge"),
"state-label": () => import("../badges/hui-state-label-badge"),
"power-total": () => import("../badges/energy/hui-power-total-badge"),
"gas-total": () => import("../badges/energy/hui-gas-total-badge"),
"water-total": () => import("../badges/energy/hui-water-total-badge"),
};
// This will not return an error card but will throw the error

View File

@@ -71,6 +71,9 @@ const LAZY_LOAD_TYPES = {
import("../cards/water/hui-water-flow-sankey-card"),
"power-sources-graph": () =>
import("../cards/energy/hui-power-sources-graph-card"),
"power-total": () => import("../cards/energy/hui-power-total-card"),
"water-total": () => import("../cards/energy/hui-water-total-card"),
"gas-total": () => import("../cards/energy/hui-gas-total-card"),
"power-sankey": () => import("../cards/energy/hui-power-sankey-card"),
"entity-filter": () => import("../cards/hui-entity-filter-card"),
error: () => import("../cards/hui-error-card"),

View File

@@ -6,7 +6,6 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import "../../../components/ha-spinner";
import type { HistoryStates } from "../../../data/history";
import { subscribeHistoryStatesTimeWindow } from "../../../data/history";
import type { HomeAssistant } from "../../../types";
import { findEntities } from "../common/find-entities";
@@ -67,8 +66,6 @@ export class HuiGraphHeaderFooter
private _error?: string;
private _history?: HistoryStates;
private _interval?: number;
private _subscribed?: Promise<(() => Promise<void>) | undefined>;
@@ -164,8 +161,24 @@ export class HuiGraphHeaderFooter
// Message came in before we had a chance to unload
return;
}
this._history = combinedHistory;
this._computeCoordinates();
const width = this.clientWidth || this.offsetWidth;
// sample to 1 point per hour or 1 point per 5 pixels
const maxDetails = Math.max(
10,
this._config.detail! > 1
? Math.max(width / 5, this._config.hours_to_show!)
: this._config.hours_to_show!
);
const useMean = this._config.detail !== 2;
const { points } = coordinatesMinimalResponseCompressedState(
combinedHistory[this._config.entity],
width,
width / 5,
maxDetails,
{ minY: this._config.limits?.min, maxY: this._config.limits?.max },
useMean
);
this._coordinates = points;
},
this._config.hours_to_show!,
[this._config.entity]
@@ -177,63 +190,10 @@ export class HuiGraphHeaderFooter
this._setRedrawTimer();
}
private _computeCoordinates() {
if (!this._history || !this._config) {
return;
}
const entityHistory = this._history[this._config.entity];
if (!entityHistory?.length) {
return;
}
const width = this.clientWidth || this.offsetWidth;
// sample to 1 point per hour or 1 point per 5 pixels
const maxDetails = Math.max(
10,
this._config.detail! > 1
? Math.max(width / 5, this._config.hours_to_show!)
: this._config.hours_to_show!
);
const useMean = this._config.detail !== 2;
const { points } = coordinatesMinimalResponseCompressedState(
entityHistory,
width,
width / 5,
maxDetails,
{ minY: this._config.limits?.min, maxY: this._config.limits?.max },
useMean
);
this._coordinates = points;
}
private _redrawGraph() {
if (!this._history || !this._config?.hours_to_show) {
return;
if (this._coordinates) {
this._coordinates = [...this._coordinates];
}
const entityId = this._config.entity;
const entityHistory = this._history[entityId];
if (entityHistory?.length) {
const purgeBeforeTimestamp =
(Date.now() - this._config.hours_to_show * 60 * 60 * 1000) / 1000;
let purgedHistory = entityHistory.filter(
(entry) => entry.lu >= purgeBeforeTimestamp
);
if (purgedHistory.length !== entityHistory.length) {
if (
!purgedHistory.length ||
purgedHistory[0].lu !== purgeBeforeTimestamp
) {
// Preserve the last expired state as the start boundary
const lastExpiredState = {
...entityHistory[entityHistory.length - purgedHistory.length - 1],
};
lastExpiredState.lu = purgeBeforeTimestamp;
delete lastExpiredState.lc;
purgedHistory = [lastExpiredState, ...purgedHistory];
}
this._history = { ...this._history, [entityId]: purgedHistory };
}
}
this._computeCoordinates();
}
private _setRedrawTimer() {
@@ -251,7 +211,6 @@ export class HuiGraphHeaderFooter
this._subscribed.then((unsub) => unsub?.());
this._subscribed = undefined;
}
this._history = undefined;
}
protected updated(changedProps: PropertyValues) {

View File

@@ -631,8 +631,11 @@ class HUIRoot extends LitElement {
`;
}
private _handleWindowScroll = () => {
this.toggleAttribute("scrolled", window.scrollY !== 0);
private _handleContainerScroll = () => {
this.toggleAttribute(
"scrolled",
this._viewRoot ? this._viewRoot.scrollTop !== 0 : false
);
};
private _locationChanged = () => {
@@ -663,7 +666,7 @@ class HUIRoot extends LitElement {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
window.addEventListener("scroll", this._handleWindowScroll, {
this._viewRoot?.addEventListener("scroll", this._handleContainerScroll, {
passive: true,
});
this._handleUrlChanged();
@@ -674,7 +677,7 @@ class HUIRoot extends LitElement {
public connectedCallback(): void {
super.connectedCallback();
window.addEventListener("scroll", this._handleWindowScroll, {
this._viewRoot?.addEventListener("scroll", this._handleContainerScroll, {
passive: true,
});
window.addEventListener("popstate", this._handlePopState);
@@ -685,10 +688,13 @@ class HUIRoot extends LitElement {
public disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener("scroll", this._handleWindowScroll);
this._viewRoot?.removeEventListener("scroll", this._handleContainerScroll);
window.removeEventListener("popstate", this._handlePopState);
window.removeEventListener("location-changed", this._locationChanged);
this.toggleAttribute("scrolled", window.scrollY !== 0);
this.toggleAttribute(
"scrolled",
this._viewRoot ? this._viewRoot.scrollTop !== 0 : false
);
// Re-enable history scroll restoration when leaving the page
window.history.scrollRestoration = "auto";
}
@@ -821,9 +827,11 @@ class HUIRoot extends LitElement {
(this._restoreScroll && this._viewScrollPositions[newSelectView]) ||
0;
this._restoreScroll = false;
requestAnimationFrame(() =>
scrollTo({ behavior: "auto", top: position })
);
requestAnimationFrame(() => {
if (this._viewRoot) {
this._viewRoot.scrollTo({ behavior: "auto", top: position });
}
});
}
this._selectView(newSelectView, force);
});
@@ -1148,7 +1156,7 @@ class HUIRoot extends LitElement {
const path = this.config.views[viewIndex].path || viewIndex;
this._navigateToView(path);
} else if (!this._editMode) {
scrollTo({ behavior: "smooth", top: 0 });
this._viewRoot?.scrollTo({ behavior: "smooth", top: 0 });
}
}
@@ -1159,7 +1167,7 @@ class HUIRoot extends LitElement {
// Save scroll position of current view
if (this._curView != null) {
this._viewScrollPositions[this._curView] = window.scrollY;
this._viewScrollPositions[this._curView] = this._viewRoot?.scrollTop ?? 0;
}
viewIndex = viewIndex === undefined ? 0 : viewIndex;
@@ -1467,9 +1475,14 @@ class HUIRoot extends LitElement {
hui-view-container {
position: relative;
display: flex;
min-height: 100vh;
height: calc(
100vh - var(--header-height) - var(--safe-area-inset-top) - var(
--view-container-padding-top,
0px
)
);
box-sizing: border-box;
padding-top: calc(
margin-top: calc(
var(--header-height) + var(--safe-area-inset-top) +
var(--view-container-padding-top, 0px)
);
@@ -1485,8 +1498,6 @@ class HUIRoot extends LitElement {
padding-inline-start: var(--safe-area-inset-left);
}
hui-view-container > * {
display: flex;
flex-direction: column;
flex: 1 1 100%;
max-width: 100%;
}
@@ -1494,7 +1505,12 @@ class HUIRoot extends LitElement {
* In edit mode we have the tab bar on a new line *
*/
hui-view-container.has-tab-bar {
padding-top: calc(
height: calc(
100vh - var(--header-height, 56px) - calc(
var(--tab-bar-height, 56px) - 2px
) - var(--safe-area-inset-top, 0px)
);
margin-top: calc(
var(--header-height, 56px) +
calc(var(--tab-bar-height, 56px) - 2px) +
var(--safe-area-inset-top, 0px)

View File

@@ -418,11 +418,15 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
}
private _toggleView() {
// The scroll container is the hui-view-container parent
const scrollContainer = this.closest("hui-view-container");
const scrollTop = scrollContainer?.scrollTop ?? 0;
// Save current scroll position
if (this._sidebarTabActive) {
this._sidebarScrollTop = window.scrollY;
this._sidebarScrollTop = scrollTop;
} else {
this._contentScrollTop = window.scrollY;
this._contentScrollTop = scrollTop;
}
this._sidebarTabActive = !this._sidebarTabActive;
@@ -438,7 +442,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
const scrollY = this._sidebarTabActive
? this._sidebarScrollTop
: this._contentScrollTop;
window.scrollTo(0, scrollY);
scrollContainer?.scrollTo(0, scrollY);
});
}
@@ -461,7 +465,6 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
--column-min-width: var(--ha-view-sections-column-min-width, 320px);
--top-margin: var(--ha-view-sections-extra-top-margin, 80px);
display: block;
flex: 1;
}
@media (max-width: 600px) {
@@ -471,9 +474,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
}
.wrapper {
display: flex;
flex-direction: column;
min-height: calc(100% - 2 * var(--row-gap));
display: block;
padding: var(--row-gap) var(--column-gap);
box-sizing: content-box;
margin: 0 auto;
@@ -506,7 +507,6 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
gap: var(--row-gap) var(--column-gap);
padding: var(--row-gap) 0;
align-items: flex-start;
flex: 1 0 auto;
}
.wrapper.has-sidebar .container {

View File

@@ -4,6 +4,7 @@ import { customElement, property, state } from "lit/decorators";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { listenMediaQuery } from "../../../common/dom/media_query";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import { haStyleScrollbar } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
type BackgroundConfig = LovelaceViewConfig["background"];
@@ -22,6 +23,7 @@ class HuiViewContainer extends LitElement {
public connectedCallback(): void {
super.connectedCallback();
this.classList.add("ha-scrollbar");
this._setUpMediaQuery();
this._applyTheme();
}
@@ -74,11 +76,16 @@ class HuiViewContainer extends LitElement {
}
}
static styles = css`
:host {
display: relative;
}
`;
static styles = [
haStyleScrollbar,
css`
:host {
display: block;
height: 100%;
-webkit-overflow-scrolling: touch;
}
`,
];
}
declare global {

View File

@@ -102,14 +102,6 @@ export const getMyRedirects = (): Redirects => ({
component: "zwave_js",
redirect: "/config/zwave_js/dashboard",
},
config_matter: {
component: "matter",
redirect: "/config/matter/dashboard",
},
config_thread: {
component: "thread",
redirect: "/config/thread",
},
add_zigbee_device: {
component: "zha",
redirect: "/config/zha/add",

View File

@@ -244,17 +244,20 @@ export const haStyleDialogFixedTop = css`
`;
export const haStyleScrollbar = css`
.ha-scrollbar::-webkit-scrollbar {
.ha-scrollbar::-webkit-scrollbar,
:host(.ha-scrollbar)::-webkit-scrollbar {
width: 0.4rem;
height: 0.4rem;
}
.ha-scrollbar::-webkit-scrollbar-thumb {
.ha-scrollbar::-webkit-scrollbar-thumb,
:host(.ha-scrollbar)::-webkit-scrollbar-thumb {
border-radius: var(--ha-border-radius-sm);
background: var(--scrollbar-thumb-color);
}
.ha-scrollbar {
.ha-scrollbar,
:host(.ha-scrollbar) {
overflow-y: auto;
scrollbar-color: var(--scrollbar-thumb-color) transparent;
scrollbar-width: thin;

View File

@@ -359,8 +359,6 @@ export const darkColorStyles = css`
--outline-hover-color: rgba(225, 225, 225, 0.24);
--shadow-color: rgba(0, 0, 0, 0.48);
--scrollbar-thumb-color: rgb(110, 110, 110);
--mdc-ripple-color: #aaaaaa;
--mdc-linear-progress-buffer-color: rgba(255, 255, 255, 0.1);

View File

@@ -51,21 +51,6 @@ export const mainStyles = css`
/* dialog backdrop filter */
--ha-dialog-scrim-backdrop-filter: brightness(68%);
/* scrollbar */
scrollbar-color: var(--scrollbar-thumb-color) transparent;
scrollbar-width: thin;
}
html::-webkit-scrollbar {
width: 0.4rem;
height: 0.4rem;
}
html::-webkit-scrollbar-thumb {
border-radius: var(--ha-border-radius-sm);
background: var(--scrollbar-thumb-color);
border: 3px solid transparent;
}
`;

View File

@@ -548,7 +548,7 @@
},
"template": {
"yaml_warning": "It appears you may be writing YAML into this template field (saw ''{string}''), which is likely incorrect. This field is intended for templates only (e.g. '{{ states(sensor.test) > 0 }}' ).",
"learn_more": "Learn more about templating"
"learn_more": "Learn more about templating."
},
"text": {
"show_password": "Show password",
@@ -679,9 +679,9 @@
"no_entities": "You don't have any entities",
"no_match": "No entities found for {term}",
"show_entities": "Show entities",
"new_entity": "Creates a new entity",
"new_entity": "Create a new entity",
"placeholder": "Select an entity",
"create_helper": "Create a new {domain, select, \n undefined {} \n other {{domain} }\n }helper",
"create_helper": "Create a new {domain, select, \n undefined {} \n other {{domain} }\n } helper.",
"unknown": "Unknown entity selected"
},
"entity-name-picker": {
@@ -1524,7 +1524,6 @@
"settings": "Settings",
"edit": "Edit entity",
"details": "Details",
"toggle_yaml_mode": "Toggle YAML mode",
"translated": "Translated",
"raw": "Raw",
"back_to_info": "Back to info",
@@ -3812,7 +3811,7 @@
"grid": {
"title": "Electricity grid",
"sub": "Configure the amount of energy that you consume from the grid and, if you produce energy, give back to the grid. This allows Home Assistant to track your whole home energy usage.",
"learn_more": "More information on how to get started",
"learn_more": "More information on how to get started.",
"grid_connections": "Grid connections",
"add_connection": "Add grid connection",
"edit_connection": "Edit grid connection",
@@ -3873,7 +3872,7 @@
"solar": {
"title": "Solar panels",
"sub": "Let Home Assistant monitor your solar panels and give you insight on their performance.",
"learn_more": "More information on how to get started",
"learn_more": "More information on how to get started.",
"solar_production": "Solar production",
"edit_solar_production": "Edit solar production",
"delete_solar_production": "Remove solar production",
@@ -3896,7 +3895,7 @@
"battery": {
"title": "Home battery storage",
"sub": "If you have a battery system, you can configure it to monitor how much energy was stored and used from your battery.",
"learn_more": "More information on how to get started",
"learn_more": "More information on how to get started.",
"battery_systems": "Battery systems",
"edit_battery_system": "Edit battery system",
"delete_battery_system": "Remove battery system",
@@ -3923,7 +3922,7 @@
"gas": {
"title": "Gas consumption",
"sub": "Let Home Assistant monitor your gas usage.",
"learn_more": "More information on how to get started",
"learn_more": "More information on how to get started.",
"gas_consumption": "Gas consumption",
"edit_gas_source": "Edit gas source",
"delete_gas_source": "Remove gas source",
@@ -3952,7 +3951,7 @@
"water": {
"title": "Water consumption",
"sub": "Let Home Assistant monitor your water usage.",
"learn_more": "More information on how to get started",
"learn_more": "More information on how to get started.",
"water_consumption": "Water consumption",
"edit_water_source": "Edit water source",
"delete_water_source": "Remove water source",
@@ -3978,7 +3977,7 @@
"device_consumption": {
"title": "Individual electrical devices",
"sub": "Tracking the energy usage of individual devices allows Home Assistant to break down your energy usage by device.",
"learn_more": "More information on how to get started",
"learn_more": "More information on how to get started.",
"add_stat": "Pick entity to track energy of",
"selected_stat": "Tracking energy for",
"devices": "Devices",
@@ -3997,7 +3996,7 @@
"device_consumption_water": {
"title": "Individual water devices",
"sub": "Tracking the water usage of individual devices allows Home Assistant to break down your water usage by device.",
"learn_more": "More information on how to get started",
"learn_more": "More information on how to get started.",
"devices": "Devices",
"add_device": "Add device",
"dialog": {
@@ -5749,7 +5748,7 @@
"key_not_null": "The field key must not be empty.",
"key_not_unique": "The field key must not be the same value as another field.",
"fields": "Fields",
"link_help_fields": "Learn more about fields",
"link_help_fields": "Learn more about fields.",
"add_fields": "Add fields",
"add_field": "Add field",
"field": "field",
@@ -5777,7 +5776,7 @@
"delete_confirm_text": "{name} will be permanently deleted.",
"sequence": "Sequence",
"sequence_sentence": "The sequence of actions of this script.",
"link_available_actions": "Learn more about available actions",
"link_available_actions": "Learn more about available actions.",
"leave": {
"unsaved_new_title": "Save new script?",
"unsaved_new_text": "You can save your changes, or delete this script. You can't undo this action.",
@@ -6050,7 +6049,7 @@
"no_hooks_yet_link_integration": "webhook-based integration",
"no_hooks_yet2": " or by creating a ",
"no_hooks_yet_link_automation": "webhook automation",
"link_learn_more": "Learn more about creating webhook-powered automations",
"link_learn_more": "Learn more about creating webhook-powered automations.",
"loading": "Loading…",
"manage": "Manage",
"disable_hook_error_msg": "Failed to disable webhook:"

265
yarn.lock
View File

@@ -1990,75 +1990,74 @@ __metadata:
languageName: node
linkType: hard
"@html-eslint/core@npm:^0.57.0":
version: 0.57.0
resolution: "@html-eslint/core@npm:0.57.0"
"@html-eslint/core@npm:^0.56.0":
version: 0.56.0
resolution: "@html-eslint/core@npm:0.56.0"
dependencies:
"@html-eslint/types": "npm:^0.57.0"
"@html-eslint/types": "npm:^0.56.0"
eslint: "npm:^9.39.1"
html-standard: "npm:^0.0.13"
checksum: 10/18b7aaf455969008dc5b79f6fd4b38792d893053e77c098c56a9238fe9b3ac3fa36441a88b908ade71de2f5179d5b4c45f552d7da5beff1d5c8df9cb3fd14e63
checksum: 10/0722e7405e56b256c471229dbd0ccb49cf0edd3e5224a087240f1954c74af0a6f289e5be0b83957480f1fa5abf6f7cbad7212134a6872a3fd66e1dc1ad879d66
languageName: node
linkType: hard
"@html-eslint/eslint-plugin@npm:0.57.0":
version: 0.57.0
resolution: "@html-eslint/eslint-plugin@npm:0.57.0"
"@html-eslint/eslint-plugin@npm:0.56.0":
version: 0.56.0
resolution: "@html-eslint/eslint-plugin@npm:0.56.0"
dependencies:
"@eslint/plugin-kit": "npm:^0.4.1"
"@html-eslint/core": "npm:^0.57.0"
"@html-eslint/parser": "npm:^0.57.0"
"@html-eslint/template-parser": "npm:^0.57.0"
"@html-eslint/template-syntax-parser": "npm:^0.57.0"
"@html-eslint/types": "npm:^0.57.0"
"@rviscomi/capo.js": "npm:^2.1.0"
"@html-eslint/core": "npm:^0.56.0"
"@html-eslint/parser": "npm:^0.56.0"
"@html-eslint/template-parser": "npm:^0.56.0"
"@html-eslint/template-syntax-parser": "npm:^0.56.0"
"@html-eslint/types": "npm:^0.56.0"
peerDependencies:
eslint: ">=8.0.0 || ^10.0.0-0"
checksum: 10/34cf11eaab3c07436c9c8b2896ed331c3ba5d591db5d8a8ff79d19b174e6e090323eb71e1368ad93d2afa945d4740749c599436a03be1195d4e4a4b0d3d9e9b6
checksum: 10/16c5d638045266ca201b5dc154b3706262801668b2edde4f0905bdba9ba2e14b7175eacf9ca7d2ad5a5a03e3369843822d34ee607b6b7fbbeb020ac7f87629fb
languageName: node
linkType: hard
"@html-eslint/parser@npm:^0.57.0":
version: 0.57.0
resolution: "@html-eslint/parser@npm:0.57.0"
"@html-eslint/parser@npm:^0.56.0":
version: 0.56.0
resolution: "@html-eslint/parser@npm:0.56.0"
dependencies:
"@eslint/css-tree": "npm:^3.6.9"
"@html-eslint/template-syntax-parser": "npm:^0.57.0"
"@html-eslint/types": "npm:^0.57.0"
"@html-eslint/template-syntax-parser": "npm:^0.56.0"
"@html-eslint/types": "npm:^0.56.0"
css-tree: "npm:^3.1.0"
es-html-parser: "npm:0.3.1"
checksum: 10/d742a26ef8122eab82e35ab27de6fe40cd45d2ac59ade9bed9d16df0da3cd18425c7892fedd245b9d60da2a4fbfb8c6afe05b1b6415033ab8fad996b94be020e
checksum: 10/1fead2be97481f461ee7030f4ea9274e5f969540118b4429e3eea361f62138660c1b7538668c7f8d55a8ae6985deff3056968d71bae62b91fb001d4209a24b50
languageName: node
linkType: hard
"@html-eslint/template-parser@npm:^0.57.0":
version: 0.57.0
resolution: "@html-eslint/template-parser@npm:0.57.0"
"@html-eslint/template-parser@npm:^0.56.0":
version: 0.56.0
resolution: "@html-eslint/template-parser@npm:0.56.0"
dependencies:
"@html-eslint/types": "npm:^0.57.0"
"@html-eslint/types": "npm:^0.56.0"
es-html-parser: "npm:0.3.1"
checksum: 10/160d7f6156f242ed7bd4e7f13d79c350bf5db7fef089ba7b404ef770d80547620e9ee860ead8892a4079668db1cdccf6a6c6b91183a8c478a634314b39efea32
checksum: 10/45e41dc8fb46336f40d1b78ef55ee3150d7a1034d393529eb40ce37a7d0a8fa04f6ed8f9a63478ff884d9588b1359b97a799681a37cd485d237dce867b7f9d20
languageName: node
linkType: hard
"@html-eslint/template-syntax-parser@npm:^0.57.0":
version: 0.57.0
resolution: "@html-eslint/template-syntax-parser@npm:0.57.0"
"@html-eslint/template-syntax-parser@npm:^0.56.0":
version: 0.56.0
resolution: "@html-eslint/template-syntax-parser@npm:0.56.0"
dependencies:
"@html-eslint/types": "npm:^0.57.0"
checksum: 10/147d8d15ebc5ea9b75eea04463b9f29cfc476be2e98c5a2129442be6f66d32a9e8bb11a1be13cc3778df25e14d7ed1cd345342951ecec05292545f64bb3d92c6
"@html-eslint/types": "npm:^0.56.0"
checksum: 10/2cc4f2cbfa58c6381b2542ca86e424a97429326aa0b8b6b09e0a51d27b0e26fd723a4550e4c17536e00cfd506cbd0c61d8fd3dff1c829714c4432406096b4a01
languageName: node
linkType: hard
"@html-eslint/types@npm:^0.57.0":
version: 0.57.0
resolution: "@html-eslint/types@npm:0.57.0"
"@html-eslint/types@npm:^0.56.0":
version: 0.56.0
resolution: "@html-eslint/types@npm:0.56.0"
dependencies:
"@types/css-tree": "npm:^2.3.11"
"@types/estree": "npm:^1.0.6"
es-html-parser: "npm:0.3.1"
eslint: "npm:^9.39.1"
checksum: 10/44d0bde592786a2da966beab13c3d543e6669b1e38b6e5feea0f01121eec928be0081969df3ada1f27c9bd43d6286596afa56a2e075da843251079a940cf0825
checksum: 10/5359ccfb8f431ccedfe7d9c0b20d883d19bbb6a24eee925c036c7774d6f2177eda021826abbc6f947b59c2364d7cf25a596591e852c1141000a3fab6ea70acc9
languageName: node
linkType: hard
@@ -4258,13 +4257,6 @@ __metadata:
languageName: node
linkType: hard
"@rviscomi/capo.js@npm:^2.1.0":
version: 2.1.0
resolution: "@rviscomi/capo.js@npm:2.1.0"
checksum: 10/83ce30106ec6bd4b4791af12556119c568cb9d0ec7550b7b015a8c2c1c49687d8c6605564415d9d85e991581958212109ee5caa0db4166c1d3628033175e80b8
languageName: node
linkType: hard
"@shoelace-style/animations@npm:^1.2.0":
version: 1.2.0
resolution: "@shoelace-style/animations@npm:1.2.0"
@@ -5071,138 +5063,138 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/eslint-plugin@npm:8.56.1":
version: 8.56.1
resolution: "@typescript-eslint/eslint-plugin@npm:8.56.1"
"@typescript-eslint/eslint-plugin@npm:8.56.0":
version: 8.56.0
resolution: "@typescript-eslint/eslint-plugin@npm:8.56.0"
dependencies:
"@eslint-community/regexpp": "npm:^4.12.2"
"@typescript-eslint/scope-manager": "npm:8.56.1"
"@typescript-eslint/type-utils": "npm:8.56.1"
"@typescript-eslint/utils": "npm:8.56.1"
"@typescript-eslint/visitor-keys": "npm:8.56.1"
"@typescript-eslint/scope-manager": "npm:8.56.0"
"@typescript-eslint/type-utils": "npm:8.56.0"
"@typescript-eslint/utils": "npm:8.56.0"
"@typescript-eslint/visitor-keys": "npm:8.56.0"
ignore: "npm:^7.0.5"
natural-compare: "npm:^1.4.0"
ts-api-utils: "npm:^2.4.0"
peerDependencies:
"@typescript-eslint/parser": ^8.56.1
"@typescript-eslint/parser": ^8.56.0
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/669d19cff91fcad5fe34dba97cc8c0c2df3160ae14646759fb23dfd6ffbb861d00d8d081e74d1060d544bfba0ea4d05588c5b73ae104907af26cc18189c0d139
checksum: 10/44201eae518c759cf3110f7e0a374372ef22bffa3cca61685aebe916b06bcb5f3ed6ffedba252199dca0006dfc22c54b132cd0337fd15e8c083eda22f9cf6b0c
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:8.56.1":
version: 8.56.1
resolution: "@typescript-eslint/parser@npm:8.56.1"
"@typescript-eslint/parser@npm:8.56.0":
version: 8.56.0
resolution: "@typescript-eslint/parser@npm:8.56.0"
dependencies:
"@typescript-eslint/scope-manager": "npm:8.56.1"
"@typescript-eslint/types": "npm:8.56.1"
"@typescript-eslint/typescript-estree": "npm:8.56.1"
"@typescript-eslint/visitor-keys": "npm:8.56.1"
"@typescript-eslint/scope-manager": "npm:8.56.0"
"@typescript-eslint/types": "npm:8.56.0"
"@typescript-eslint/typescript-estree": "npm:8.56.0"
"@typescript-eslint/visitor-keys": "npm:8.56.0"
debug: "npm:^4.4.3"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/280b041a69153caf9e721b307781830483dd39d881b02d993156635bd8600e30e6a816aaead8bdd662ae5149b8870aef7b3823d3b98ec974d924c23a786fb6d9
checksum: 10/9bdb2c7915665a1031499049974997020bbc34557803a8c3718b323d5583a3fdfc27797ec714b617743225c8dce9ab589eb442b71c435b464ad45365a6fbba57
languageName: node
linkType: hard
"@typescript-eslint/project-service@npm:8.56.1":
version: 8.56.1
resolution: "@typescript-eslint/project-service@npm:8.56.1"
"@typescript-eslint/project-service@npm:8.56.0":
version: 8.56.0
resolution: "@typescript-eslint/project-service@npm:8.56.0"
dependencies:
"@typescript-eslint/tsconfig-utils": "npm:^8.56.1"
"@typescript-eslint/types": "npm:^8.56.1"
"@typescript-eslint/tsconfig-utils": "npm:^8.56.0"
"@typescript-eslint/types": "npm:^8.56.0"
debug: "npm:^4.4.3"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/5e7fdc95aebcefc72fec77806bb0821a9a59e5e88f86d72b15ad011eb6110da05419b803875f021716a219fc7fb8517331a6736364344c8613a90209539a6d32
checksum: 10/b46cc78bfb50ee84cb12e2e99c1a3d9606161980cf56ba33be6244ccff2ba4c78ceec46706ad597508fda167e0f965d849c1c516400ad41259027be3fbd815eb
languageName: node
linkType: hard
"@typescript-eslint/scope-manager@npm:8.56.1":
version: 8.56.1
resolution: "@typescript-eslint/scope-manager@npm:8.56.1"
"@typescript-eslint/scope-manager@npm:8.56.0":
version: 8.56.0
resolution: "@typescript-eslint/scope-manager@npm:8.56.0"
dependencies:
"@typescript-eslint/types": "npm:8.56.1"
"@typescript-eslint/visitor-keys": "npm:8.56.1"
checksum: 10/f358cf8bd32952eed005d4f34c1e95805baefe35abee96d866222b0eff8027cc02f831cee04b308707d74db2b415437a134191207b4213ee8acbc6d67a435616
"@typescript-eslint/types": "npm:8.56.0"
"@typescript-eslint/visitor-keys": "npm:8.56.0"
checksum: 10/3662355120ea8e21ce01c999decbd2a09fe4edb1c01e376fe347952d968ecfedff99b9484334e133e41284a15f2e1bc8efd490b1e73a16980614445c25b07b0d
languageName: node
linkType: hard
"@typescript-eslint/tsconfig-utils@npm:8.56.1, @typescript-eslint/tsconfig-utils@npm:^8.56.1":
version: 8.56.1
resolution: "@typescript-eslint/tsconfig-utils@npm:8.56.1"
"@typescript-eslint/tsconfig-utils@npm:8.56.0, @typescript-eslint/tsconfig-utils@npm:^8.56.0":
version: 8.56.0
resolution: "@typescript-eslint/tsconfig-utils@npm:8.56.0"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/d509f1ae4b14969173e498db6d15c833b6407db456c7fca9e25396798a35014229a73754691f353c4a99f5f0bbb4535b4144f42f84e596645a16d88a2022135f
checksum: 10/b1834aeffcdc07835eae0bf52aca573cba7e6528b5c1483e9b1f7f4f9e1f6450a8650796be11140e0437caf7eb1b0f9711c22989c8294547534f12614a759760
languageName: node
linkType: hard
"@typescript-eslint/type-utils@npm:8.56.1":
version: 8.56.1
resolution: "@typescript-eslint/type-utils@npm:8.56.1"
"@typescript-eslint/type-utils@npm:8.56.0":
version: 8.56.0
resolution: "@typescript-eslint/type-utils@npm:8.56.0"
dependencies:
"@typescript-eslint/types": "npm:8.56.1"
"@typescript-eslint/typescript-estree": "npm:8.56.1"
"@typescript-eslint/utils": "npm:8.56.1"
"@typescript-eslint/types": "npm:8.56.0"
"@typescript-eslint/typescript-estree": "npm:8.56.0"
"@typescript-eslint/utils": "npm:8.56.0"
debug: "npm:^4.4.3"
ts-api-utils: "npm:^2.4.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/2b07c674c26d797d05c05779ac5c89761b6b96680ecaf01440957727d12c6d06a2e48f0b139e45752eb4b53bf13c03940628656c519d362082b716d6a0ece6d9
checksum: 10/f272b9acc004f125cbf0df18265a43ba50cd3666262afc663585acdd1be6b6b30724bd8cf4cd5aa2757b7f10ceafa92fd1af30c1931fb22ac38521eda7f79c89
languageName: node
linkType: hard
"@typescript-eslint/types@npm:8.56.1, @typescript-eslint/types@npm:^8.56.1":
version: 8.56.1
resolution: "@typescript-eslint/types@npm:8.56.1"
checksum: 10/4bcffab5b0fd48adb731fcade86a776ca4a66e229defa5a282b58ba9c95af16ffc459a7d188e27c988a35be1f6fb5b812f9cf0952692eac38d5b3e87daafb20a
"@typescript-eslint/types@npm:8.56.0, @typescript-eslint/types@npm:^8.56.0":
version: 8.56.0
resolution: "@typescript-eslint/types@npm:8.56.0"
checksum: 10/d7549535c99d9202742bf0191bcc2822c2d18a03e206be9ad5a6f6b0902de7381c93e8c238754fe5d1dfdcc22d7e3bbafa032f63ba165d6dc03e180f84b138f9
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:8.56.1":
version: 8.56.1
resolution: "@typescript-eslint/typescript-estree@npm:8.56.1"
"@typescript-eslint/typescript-estree@npm:8.56.0":
version: 8.56.0
resolution: "@typescript-eslint/typescript-estree@npm:8.56.0"
dependencies:
"@typescript-eslint/project-service": "npm:8.56.1"
"@typescript-eslint/tsconfig-utils": "npm:8.56.1"
"@typescript-eslint/types": "npm:8.56.1"
"@typescript-eslint/visitor-keys": "npm:8.56.1"
"@typescript-eslint/project-service": "npm:8.56.0"
"@typescript-eslint/tsconfig-utils": "npm:8.56.0"
"@typescript-eslint/types": "npm:8.56.0"
"@typescript-eslint/visitor-keys": "npm:8.56.0"
debug: "npm:^4.4.3"
minimatch: "npm:^10.2.2"
minimatch: "npm:^9.0.5"
semver: "npm:^7.7.3"
tinyglobby: "npm:^0.2.15"
ts-api-utils: "npm:^2.4.0"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/af39dae0a8fada72295a11f0efb49f311241134b0a3d819100eeda6a2f92368844645873ba785de5513ad541cd9c2ba17b9bfed2679daac4682fa2a3b627f087
checksum: 10/55c8cfc7e265f320d780e69a677838821225fad8b853108ce2095f01509bf2ee8943280df9ac75560ed86265ef0e0a979ae4cb375d712f648b336032de79d19b
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:8.56.1":
version: 8.56.1
resolution: "@typescript-eslint/utils@npm:8.56.1"
"@typescript-eslint/utils@npm:8.56.0":
version: 8.56.0
resolution: "@typescript-eslint/utils@npm:8.56.0"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.9.1"
"@typescript-eslint/scope-manager": "npm:8.56.1"
"@typescript-eslint/types": "npm:8.56.1"
"@typescript-eslint/typescript-estree": "npm:8.56.1"
"@typescript-eslint/scope-manager": "npm:8.56.0"
"@typescript-eslint/types": "npm:8.56.0"
"@typescript-eslint/typescript-estree": "npm:8.56.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/528cbd187d8288a8cfce24a043f993b93711087f53d2b6f95cdd615a1a4087af1dab083a747761af97474a621c7b14f11c84ee50c250f31566ebc64cf73867fe
checksum: 10/f357bd15fe568cba0b89371e9a724eda38d78361a21dc0c4f49b0af4a23a140c77e2a8c6285c6fe8d8277e256a8a137aef7bcf6d97428eecd0c6e72ef08849ae
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:8.56.1":
version: 8.56.1
resolution: "@typescript-eslint/visitor-keys@npm:8.56.1"
"@typescript-eslint/visitor-keys@npm:8.56.0":
version: 8.56.0
resolution: "@typescript-eslint/visitor-keys@npm:8.56.0"
dependencies:
"@typescript-eslint/types": "npm:8.56.1"
"@typescript-eslint/types": "npm:8.56.0"
eslint-visitor-keys: "npm:^5.0.0"
checksum: 10/efed6a9867e7be203eec543e5a65da5aaec96aae55fba6fe74a305bf600e57c707764835e82bb8eb541f49a9b70442ff1e1a0ecf3543c78c91b84dda43b95c53
checksum: 10/1eaa26ffe8a2c83d42d428beef207d793aef73c2e306f94e716d39519eaaa07547da898c7d63a5c406a3662895d735b4b1f33b513a1addb69473c65e7d92a2b5
languageName: node
linkType: hard
@@ -6086,12 +6078,12 @@ __metadata:
languageName: node
linkType: hard
"barcode-detector@npm:3.1.0":
version: 3.1.0
resolution: "barcode-detector@npm:3.1.0"
"barcode-detector@npm:3.0.8":
version: 3.0.8
resolution: "barcode-detector@npm:3.0.8"
dependencies:
zxing-wasm: "npm:3.0.0"
checksum: 10/60767161081b827e290b60bb3416999dee616bab39291ee55565df9b72d59f0bbbf511fd3bb85db18eee7c0ad9acf1ff90359cdb21e10f80793acd0105c86a1d
zxing-wasm: "npm:2.2.4"
checksum: 10/7de6225f659c69a0f4101d080a9e0812f2404c485fa2406424b8b13eaff274f6e7405c94de24827a09def52c32a63b19b9c9fba61c5274b074d558f696ec1684
languageName: node
linkType: hard
@@ -9200,7 +9192,7 @@ __metadata:
"@fullcalendar/luxon3": "npm:6.1.20"
"@fullcalendar/timegrid": "npm:6.1.20"
"@home-assistant/webawesome": "npm:3.2.1-ha.3"
"@html-eslint/eslint-plugin": "npm:0.57.0"
"@html-eslint/eslint-plugin": "npm:0.56.0"
"@lezer/highlight": "npm:1.2.3"
"@lit-labs/motion": "npm:1.1.0"
"@lit-labs/observers": "npm:2.1.0"
@@ -9268,7 +9260,7 @@ __metadata:
app-datepicker: "npm:5.1.1"
babel-loader: "npm:10.0.0"
babel-plugin-template-html-minifier: "npm:4.1.0"
barcode-detector: "npm:3.1.0"
barcode-detector: "npm:3.0.8"
browserslist-useragent-regexp: "npm:4.1.3"
color-name: "npm:2.1.0"
comlink: "npm:4.4.2"
@@ -9343,7 +9335,7 @@ __metadata:
tinykeys: "npm:3.0.0"
ts-lit-plugin: "npm:2.0.2"
typescript: "npm:5.9.3"
typescript-eslint: "npm:8.56.1"
typescript-eslint: "npm:8.56.0"
ua-parser-js: "npm:2.0.9"
vite-tsconfig-paths: "npm:6.1.1"
vitest: "npm:4.0.18"
@@ -11125,6 +11117,15 @@ __metadata:
languageName: node
linkType: hard
"minimatch@npm:^9.0.5":
version: 9.0.5
resolution: "minimatch@npm:9.0.5"
dependencies:
brace-expansion: "npm:^2.0.1"
checksum: 10/dd6a8927b063aca6d910b119e1f2df6d2ce7d36eab91de83167dd136bb85e1ebff97b0d3de1cb08bd1f7e018ca170b4962479fefab5b2a69e2ae12cb2edc8348
languageName: node
linkType: hard
"minimist@npm:^1.2.0, minimist@npm:^1.2.6":
version: 1.2.8
resolution: "minimist@npm:1.2.8"
@@ -14139,12 +14140,12 @@ __metadata:
languageName: node
linkType: hard
"type-fest@npm:^5.4.4":
version: 5.4.4
resolution: "type-fest@npm:5.4.4"
"type-fest@npm:^5.2.0":
version: 5.4.1
resolution: "type-fest@npm:5.4.1"
dependencies:
tagged-tag: "npm:^1.0.0"
checksum: 10/0bbdca645f95740587f389a2d712fe8d5e9ab7d13e74aac97cf396112510abcaab6b75fd90d65172bc13b02fdfc827e6a871322cc9c1c1a5a2754d9ab264c6f5
checksum: 10/be7d4749e1e5cf2e2c9904fa1aaf9da5eef6c47c130881bf93bfd5a670b2ab59c5502466768e42c521281056a2375b1617176a75cf6c52b575f4bbabbd450b21
languageName: node
linkType: hard
@@ -14211,18 +14212,18 @@ __metadata:
languageName: node
linkType: hard
"typescript-eslint@npm:8.56.1":
version: 8.56.1
resolution: "typescript-eslint@npm:8.56.1"
"typescript-eslint@npm:8.56.0":
version: 8.56.0
resolution: "typescript-eslint@npm:8.56.0"
dependencies:
"@typescript-eslint/eslint-plugin": "npm:8.56.1"
"@typescript-eslint/parser": "npm:8.56.1"
"@typescript-eslint/typescript-estree": "npm:8.56.1"
"@typescript-eslint/utils": "npm:8.56.1"
"@typescript-eslint/eslint-plugin": "npm:8.56.0"
"@typescript-eslint/parser": "npm:8.56.0"
"@typescript-eslint/typescript-estree": "npm:8.56.0"
"@typescript-eslint/utils": "npm:8.56.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/e18cd347ddce0f0e5b28121346f27736a418adf68e73d613690ea3a1d0adfe03bc393f77a8872c2cef77ca74bcc0974212d1775c360de33a9987a94cda11a05b
checksum: 10/7c07af35e6b4eaaebad4b50c37bc6dd50f0cecebe5a4e648ef117fd43a8496f3132020061e19a2fbaf826978e91c100054e638701bf89db8f342dd1353bb5b7e
languageName: node
linkType: hard
@@ -15703,14 +15704,14 @@ __metadata:
languageName: node
linkType: hard
"zxing-wasm@npm:3.0.0":
version: 3.0.0
resolution: "zxing-wasm@npm:3.0.0"
"zxing-wasm@npm:2.2.4":
version: 2.2.4
resolution: "zxing-wasm@npm:2.2.4"
dependencies:
"@types/emscripten": "npm:^1.41.5"
type-fest: "npm:^5.4.4"
type-fest: "npm:^5.2.0"
peerDependencies:
"@types/emscripten": ">=1.39.6"
checksum: 10/0acf04829acf8f3987173af011784642792fc877c7765f79222fe33efff8af09fbf95bf5d590d2490ae39ec411e6c4de06ea24e96d4eb48189b9d06f7502eaa2
checksum: 10/e5928cbb066c854c970cbf724e978e502c3469d69de2469bd37d1f6ab8f5d2a2acdbaa9dea32d35cfc058e2b482e29a9c4f12161d9df3e1e952c30dda3a96d8c
languageName: node
linkType: hard