Compare commits

..

10 Commits

Author SHA1 Message Date
Petar Petrov
d41d524850 Show battery in and out energy in Sankey chart (#26490) 2025-08-13 12:40:45 +03:00
renovate[bot]
4f05f6305a Update fullcalendar monorepo to v6.1.19 (#26516)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-13 09:50:40 +02:00
karwosts
ba0b1239be Fix search in automation yaml editor (#26513) 2025-08-13 09:07:50 +03:00
Wendelin
708b68f35d Fix handling empty release notes in more-info-update (#26515)
Fix handling empty release nots in more-info-update
2025-08-13 09:03:25 +03:00
Aidan Timson
3108e98b97 Update ha-spinner component to webawesome (#26507) 2025-08-12 21:00:40 +02:00
Bram Kragten
ba7609cc2c Fix style variable in base chart (#26509) 2025-08-12 17:14:29 +02:00
Bram Kragten
506fd7d480 center spinner 2025-08-12 15:30:47 +02:00
Bram Kragten
9767ebe1fb Show spinner when loading application credential config (#26510) 2025-08-12 15:25:18 +02:00
Aidan Timson
539e89e7b5 Add valve open/close card feature (#26488)
* Add valve open/close card feature

* Toggle button UI if no assumed state
2025-08-12 14:26:12 +02:00
karwosts
a7eef81272 Fix search in raw configuration editor (#26496) 2025-08-12 14:31:38 +03:00
19 changed files with 682 additions and 446 deletions

View File

@@ -46,12 +46,12 @@
"@formatjs/intl-numberformat": "8.15.4",
"@formatjs/intl-pluralrules": "5.4.4",
"@formatjs/intl-relativetimeformat": "11.4.11",
"@fullcalendar/core": "6.1.18",
"@fullcalendar/daygrid": "6.1.18",
"@fullcalendar/interaction": "6.1.18",
"@fullcalendar/list": "6.1.18",
"@fullcalendar/luxon3": "6.1.18",
"@fullcalendar/timegrid": "6.1.18",
"@fullcalendar/core": "6.1.19",
"@fullcalendar/daygrid": "6.1.19",
"@fullcalendar/interaction": "6.1.19",
"@fullcalendar/list": "6.1.19",
"@fullcalendar/luxon3": "6.1.19",
"@fullcalendar/timegrid": "6.1.19",
"@lezer/highlight": "1.2.1",
"@lit-labs/motion": "1.0.9",
"@lit-labs/observers": "2.0.6",
@@ -61,6 +61,7 @@
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-base": "0.27.0",
"@material/mwc-button": "0.27.0",
"@material/mwc-checkbox": "0.27.0",
"@material/mwc-dialog": "0.27.0",
"@material/mwc-drawer": "0.27.0",
@@ -231,7 +232,7 @@
"lit-html": "3.3.1",
"clean-css": "5.3.3",
"@lit/reactive-element": "2.1.1",
"@fullcalendar/daygrid": "6.1.18",
"@fullcalendar/daygrid": "6.1.19",
"globals": "16.3.0",
"tslib": "2.8.1",
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",

View File

@@ -397,7 +397,7 @@ export class HaChartBase extends LitElement {
...axis.axisPointer,
status: "show",
handle: {
color: style.getPropertyValue("primary-color"),
color: style.getPropertyValue("--primary-color"),
margin: 0,
size: 20,
...axis.axisPointer?.handle,

View File

@@ -1,82 +0,0 @@
import ButtonGroup from "@awesome.me/webawesome/dist/components/button-group/button-group";
import { customElement } from "lit/decorators";
import type { HaButton } from "./ha-button";
import { StateSet } from "../resources/polyfills/stateset";
export type Appearance = "accent" | "filled" | "outlined" | "plain";
/**
* Finds an ha-button element either as the current element or within its descendants.
* @param el - The HTML element to search from
* @returns The found HaButton element, or null if not found
*/
function findButton(el: HTMLElement) {
const selector = "ha-button";
return (el.closest(selector) ?? el.querySelector(selector)) as HaButton;
}
/**
* @element ha-button-group
* @extends {ButtonGroup}
* @summary
* Group buttons. Extend Webawesome to be able to work with ha-button tags
*
* @documentation https://webawesome.com/components/button-group
*/
@customElement("ha-button-group") // @ts-expect-error Intentionally overriding private methods
export class HaButtonGroup extends ButtonGroup {
attachInternals() {
const internals = super.attachInternals();
Object.defineProperty(internals, "states", {
value: new StateSet(this, internals.states),
});
return internals;
}
// @ts-expect-error updateClassNames is used in super class
// eslint-disable-next-line @typescript-eslint/naming-convention
private override updateClassNames() {
const slottedElements = [
...this.defaultSlot.assignedElements({ flatten: true }),
] as HTMLElement[];
this.hasOutlined = false;
slottedElements.forEach((el) => {
const index = slottedElements.indexOf(el);
const button = findButton(el);
if (button) {
if ((button as HaButton).appearance === "outlined")
this.hasOutlined = true;
if (this.size) button.setAttribute("size", this.size);
button.classList.add("wa-button-group__button");
button.classList.toggle(
"wa-button-group__horizontal",
this.orientation === "horizontal"
);
button.classList.toggle(
"wa-button-group__vertical",
this.orientation === "vertical"
);
button.classList.toggle("wa-button-group__button-first", index === 0);
button.classList.toggle(
"wa-button-group__button-inner",
index > 0 && index < slottedElements.length - 1
);
button.classList.toggle(
"wa-button-group__button-last",
index === slottedElements.length - 1
);
// use button-group variant
button.setAttribute("variant", this.variant);
}
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-button-group": HaButtonGroup;
}
}

View File

@@ -1,72 +1,141 @@
import "@material/mwc-button/mwc-button";
import type { Button } from "@material/mwc-button/mwc-button";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, queryAll } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import type { ToggleButton } from "../types";
import "./ha-svg-icon";
import "./ha-button";
import "./ha-button-group";
import "./ha-icon-button";
/**
* @element ha-button-toggle-group
*
* @summary
* A button-group with one active selection.
*
* @attr {ToggleButton[]} buttons - the button config
* @attr {string} active - The value of the currently active button.
* @attr {("small"|"medium")} size - The size of the buttons in the group.
* @attr {("brand"|"neutral"|"success"|"warning"|"danger")} variant - The variant of the buttons in the group.
*
* @fires value-changed - Dispatched when the active button changes.
*/
@customElement("ha-button-toggle-group")
export class HaButtonToggleGroup extends LitElement {
@property({ attribute: false }) public buttons!: ToggleButton[];
@property() public active?: string;
@property({ reflect: true }) size: "small" | "medium" = "medium";
@property({ attribute: "full-width", type: Boolean })
public fullWidth = false;
@property() public variant:
| "brand"
| "neutral"
| "success"
| "warning"
| "danger" = "brand";
@property({ type: Boolean }) public dense = false;
@queryAll("mwc-button") private _buttons?: Button[];
protected render(): TemplateResult {
return html`
<ha-button-group .variant=${this.variant} .size=${this.size}>
${this.buttons.map(
(button) =>
html`<ha-button
class="icon"
.value=${button.value}
@click=${this._handleClick}
.title=${button.label}
.appearance=${this.active === button.value ? "accent" : "filled"}
>
${button.iconPath
? html`<ha-svg-icon
aria-label=${button.label}
.path=${button.iconPath}
></ha-svg-icon>`
: button.label}
</ha-button>`
<div>
${this.buttons.map((button) =>
button.iconPath
? html`<ha-icon-button
.label=${button.label}
.path=${button.iconPath}
.value=${button.value}
?active=${this.active === button.value}
@click=${this._handleClick}
></ha-icon-button>`
: html`<mwc-button
style=${styleMap({
width: this.fullWidth
? `${100 / this.buttons.length}%`
: "initial",
})}
outlined
.dense=${this.dense}
.value=${button.value}
?active=${this.active === button.value}
@click=${this._handleClick}
>${button.label}</mwc-button
>`
)}
</ha-button-group>
</div>
`;
}
protected updated() {
// Work around Safari default margin that is not reset in mwc-button as of aug 2021
this._buttons?.forEach(async (button) => {
await button.updateComplete;
(
button.shadowRoot!.querySelector("button") as HTMLButtonElement
).style.margin = "0";
});
}
private _handleClick(ev): void {
this.active = ev.currentTarget.value;
fireEvent(this, "value-changed", { value: this.active });
}
static styles = css`
:host {
div {
display: flex;
--mdc-icon-button-size: var(--button-toggle-size, 36px);
--mdc-icon-size: var(--button-toggle-icon-size, 20px);
direction: ltr;
}
mwc-button {
flex: 1;
--mdc-shape-small: 0;
--mdc-button-outline-width: 1px 0 1px 1px;
--mdc-button-outline-color: var(--primary-color);
}
ha-icon-button {
border: 1px solid var(--primary-color);
border-right-width: 0px;
}
ha-icon-button,
mwc-button {
position: relative;
cursor: pointer;
}
ha-icon-button::before,
mwc-button::before {
top: 0;
left: 0;
width: 100%;
height: 100%;
position: absolute;
background-color: var(--primary-color);
opacity: 0;
pointer-events: none;
content: "";
transition:
opacity 15ms linear,
background-color 15ms linear;
}
ha-icon-button[active]::before,
mwc-button[active]::before {
opacity: 1;
}
ha-icon-button[active] {
--icon-primary-color: var(--text-primary-color);
}
mwc-button[active] {
--mdc-theme-primary: var(--text-primary-color);
}
ha-icon-button:first-child,
mwc-button:first-child {
--mdc-shape-small: 4px 0 0 4px;
border-radius: 4px 0 0 4px;
--mdc-button-outline-width: 1px;
}
mwc-button:first-child::before {
border-radius: 4px 0 0 4px;
}
ha-icon-button:last-child,
mwc-button:last-child {
border-radius: 0 4px 4px 0;
border-right-width: 1px;
--mdc-shape-small: 0 4px 4px 0;
--mdc-button-outline-width: 1px;
}
mwc-button:last-child::before {
border-radius: 0 4px 4px 0;
}
ha-icon-button:only-child,
mwc-button:only-child {
--mdc-shape-small: 4px;
border-right-width: 1px;
}
`;
}

View File

@@ -35,7 +35,7 @@ export type Appearance = "accent" | "filled" | "outlined" | "plain";
* @attr {boolean} loading - shows a loading indicator instead of the buttons label and disable buttons click.
* @attr {boolean} disabled - Disables the button and prevents user interaction.
*/
@customElement("ha-button") // @ts-expect-error Intentionally overriding private methods
@customElement("ha-button")
export class HaButton extends Button {
variant: "brand" | "neutral" | "success" | "warning" | "danger" = "brand";
@@ -47,42 +47,6 @@ export class HaButton extends Button {
return internals;
}
// @ts-expect-error handleLabelSlotChange is used in super class
// eslint-disable-next-line @typescript-eslint/naming-convention
private override handleLabelSlotChange() {
const nodes = this.labelSlot.assignedNodes({ flatten: true });
let hasIconLabel = false;
let hasIcon = false;
let text = "";
// If there's only an icon and no text, it's an icon button
[...nodes].forEach((node) => {
if (
node.nodeType === Node.ELEMENT_NODE &&
(node as HTMLElement).localName === "ha-svg-icon"
) {
hasIcon = true;
if (!hasIconLabel)
hasIconLabel = (node as HTMLElement).hasAttribute("aria-label");
}
// Concatenate text nodes
if (node.nodeType === Node.TEXT_NODE) {
text += node.textContent;
}
});
this.isIconButton = text.trim() === "" && hasIcon;
if (this.isIconButton && !hasIconLabel) {
// eslint-disable-next-line no-console
console.warn(
'Icon buttons must have a label for screen readers. Add <ha-svg-icon label="..."> to remove this warning.',
this
);
}
}
static get styles(): CSSResultGroup {
return [
Button.styles,
@@ -216,11 +180,6 @@ export class HaButton extends Button {
color: var(--wa-color-on-normal);
}
}
:host([appearance~="filled"]) .button {
color: var(--wa-color-on-normal);
background-color: var(--wa-color-fill-normal);
border-color: transparent;
}
:host([appearance~="filled"])
.button:not(.disabled):not(.loading):active {
background-color: var(--button-color-fill-normal-active);

View File

@@ -1,9 +1,10 @@
import Spinner from "@shoelace-style/shoelace/dist/components/spinner/spinner.component";
import spinnerStyles from "@shoelace-style/shoelace/dist/components/spinner/spinner.styles";
import type { PropertyValues } from "lit";
import Spinner from "@awesome.me/webawesome/dist/components/spinner/spinner";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
import { StateSet } from "../resources/polyfills/stateset";
@customElement("ha-spinner")
export class HaSpinner extends Spinner {
@property() public size?: "tiny" | "small" | "medium" | "large";
@@ -32,21 +33,31 @@ export class HaSpinner extends Spinner {
}
}
static override styles = [
spinnerStyles,
css`
:host {
--indicator-color: var(
--ha-spinner-indicator-color,
var(--primary-color)
);
--track-color: var(--ha-spinner-divider-color, var(--divider-color));
--track-width: 4px;
--speed: 3.5s;
font-size: var(--ha-spinner-size, 48px);
}
`,
];
attachInternals() {
const internals = super.attachInternals();
Object.defineProperty(internals, "states", {
value: new StateSet(this, internals.states),
});
return internals;
}
static get styles(): CSSResultGroup {
return [
Spinner.styles,
css`
:host {
--indicator-color: var(
--ha-spinner-indicator-color,
var(--primary-color)
);
--track-color: var(--ha-spinner-divider-color, var(--divider-color));
--track-width: 4px;
--speed: 3.5s;
font-size: var(--ha-spinner-size, 48px);
}
`,
];
}
}
declare global {

View File

@@ -211,6 +211,7 @@ export class HaYamlEditor extends LitElement {
}
ha-code-editor {
flex-grow: 1;
min-height: 0;
}
`,
];

View File

@@ -249,15 +249,17 @@ class MoreInfoUpdate extends LitElement {
<hr />
${this._markdownLoading ? this._renderLoader() : nothing}
`
: html`
<hr />
<ha-markdown
@content-resize=${this._markdownLoaded}
.content=${this._releaseNotes}
class=${this._markdownLoading ? "hidden" : ""}
></ha-markdown>
${this._markdownLoading ? this._renderLoader() : nothing}
`
: this._releaseNotes
? html`
<hr />
<ha-markdown
@content-resize=${this._markdownLoaded}
.content=${this._releaseNotes}
class=${this._markdownLoading ? "hidden" : ""}
></ha-markdown>
${this._markdownLoading ? this._renderLoader() : nothing}
`
: nothing
: this.stateObj.attributes.release_summary
? html`
<hr />

View File

@@ -385,22 +385,30 @@ export class HAFullCalendar extends LitElement {
if (!this._viewButtons) {
this._viewButtons = [
{
label: localize("ui.components.calendar.views.dayGridMonth"),
label: localize(
"ui.panel.lovelace.editor.card.calendar.views.dayGridMonth"
),
value: "dayGridMonth",
iconPath: mdiViewModule,
},
{
label: localize("ui.components.calendar.views.dayGridWeek"),
label: localize(
"ui.panel.lovelace.editor.card.calendar.views.dayGridWeek"
),
value: "dayGridWeek",
iconPath: mdiViewWeek,
},
{
label: localize("ui.components.calendar.views.dayGridDay"),
label: localize(
"ui.panel.lovelace.editor.card.calendar.views.dayGridDay"
),
value: "dayGridDay",
iconPath: mdiViewDay,
},
{
label: localize("ui.components.calendar.views.listWeek"),
label: localize(
"ui.panel.lovelace.editor.card.calendar.views.listWeek"
),
value: "listWeek",
iconPath: mdiViewAgenda,
},
@@ -485,6 +493,10 @@ export class HAFullCalendar extends LitElement {
--mdc-icon-button-size: 32px;
}
ha-button-toggle-group {
color: var(--primary-color);
}
ha-fab {
position: absolute;
bottom: 16px;

View File

@@ -7,6 +7,7 @@ import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-combo-box";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-fade-in";
import "../../../components/ha-markdown";
import "../../../components/ha-password-field";
import "../../../components/ha-spinner";
@@ -82,7 +83,7 @@ export class DialogAddApplicationCredential extends LitElement {
}
protected render() {
if (!this._params || !this._domains) {
if (!this._params) {
return nothing;
}
const selectedDomainName = this._params.selectedDomain
@@ -101,144 +102,159 @@ export class DialogAddApplicationCredential extends LitElement {
)
)}
>
<div>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert> `
: ""}
${this._params.selectedDomain && !this._description
? html`<p>
${this.hass.localize(
"ui.panel.config.application_credentials.editor.missing_credentials",
{
integration: selectedDomainName,
}
)}
${this._manifest?.is_built_in || this._manifest?.documentation
? html`<a
href=${this._manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this._domain}`
)
: this._manifest.documentation}
target="_blank"
rel="noreferrer"
>
${!this._config
? html`<ha-fade-in .delay=${500}>
<ha-spinner size="large"></ha-spinner>
</ha-fade-in>`
: html`<div>
${this._error
? html`<ha-alert alert-type="error"
>${this._error}</ha-alert
> `
: nothing}
${this._params.selectedDomain && !this._description
? html`<p>
${this.hass.localize(
"ui.panel.config.application_credentials.editor.missing_credentials_domain_link",
"ui.panel.config.application_credentials.editor.missing_credentials",
{
integration: selectedDomainName,
}
)}
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
</a>`
: ""}
</p>`
: ""}
${!this._params.selectedDomain || !this._description
? html`<p>
${this.hass.localize(
"ui.panel.config.application_credentials.editor.description"
)}
<a
href=${documentationUrl(
this.hass!,
"/integrations/application_credentials"
${this._manifest?.is_built_in ||
this._manifest?.documentation
? html`<a
href=${this._manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this._domain}`
)
: this._manifest.documentation}
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.application_credentials.editor.missing_credentials_domain_link",
{
integration: selectedDomainName,
}
)}
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
</a>`
: nothing}
</p>`
: nothing}
${!this._params.selectedDomain || !this._description
? html`<p>
${this.hass.localize(
"ui.panel.config.application_credentials.editor.description"
)}
<a
href=${documentationUrl(
this.hass!,
"/integrations/application_credentials"
)}
target="_blank"
rel="noreferrer"
>
${this.hass!.localize(
"ui.panel.config.application_credentials.editor.view_documentation"
)}
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
</a>
</p>`
: nothing}
${this._params.selectedDomain
? nothing
: html`<ha-combo-box
name="domain"
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.domain"
)}
.value=${this._domain}
.items=${this._domains}
item-id-path="id"
item-value-path="id"
item-label-path="name"
required
@value-changed=${this._handleDomainPicked}
></ha-combo-box>`}
${this._description
? html`<ha-markdown
breaks
.content=${this._description}
></ha-markdown>`
: nothing}
<ha-textfield
class="name"
name="name"
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.name"
)}
target="_blank"
rel="noreferrer"
>
${this.hass!.localize(
"ui.panel.config.application_credentials.editor.view_documentation"
.value=${this._name}
required
@input=${this._handleValueChanged}
.validationMessage=${this.hass.localize(
"ui.common.error_required"
)}
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
</a>
</p>`
: ""}
${this._params.selectedDomain
? ""
: html`<ha-combo-box
name="domain"
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.domain"
)}
.value=${this._domain}
.items=${this._domains}
item-id-path="id"
item-value-path="id"
item-label-path="name"
required
@value-changed=${this._handleDomainPicked}
></ha-combo-box>`}
${this._description
? html`<ha-markdown
breaks
.content=${this._description}
></ha-markdown>`
: ""}
<ha-textfield
class="name"
name="name"
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.name"
)}
.value=${this._name}
required
@input=${this._handleValueChanged}
.validationMessage=${this.hass.localize("ui.common.error_required")}
dialogInitialFocus
></ha-textfield>
<ha-textfield
class="clientId"
name="clientId"
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_id"
)}
.value=${this._clientId}
required
@input=${this._handleValueChanged}
.validationMessage=${this.hass.localize("ui.common.error_required")}
dialogInitialFocus
.helper=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_id_helper"
)}
helperPersistent
></ha-textfield>
<ha-password-field
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_secret"
)}
name="clientSecret"
.value=${this._clientSecret}
required
@input=${this._handleValueChanged}
.validationMessage=${this.hass.localize("ui.common.error_required")}
.helper=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_secret_helper"
)}
helperPersistent
></ha-password-field>
</div>
dialogInitialFocus
></ha-textfield>
<ha-textfield
class="clientId"
name="clientId"
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_id"
)}
.value=${this._clientId}
required
@input=${this._handleValueChanged}
.validationMessage=${this.hass.localize(
"ui.common.error_required"
)}
dialogInitialFocus
.helper=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_id_helper"
)}
helperPersistent
></ha-textfield>
<ha-password-field
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_secret"
)}
name="clientSecret"
.value=${this._clientSecret}
required
@input=${this._handleValueChanged}
.validationMessage=${this.hass.localize(
"ui.common.error_required"
)}
.helper=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_secret_helper"
)}
helperPersistent
></ha-password-field>
</div>
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this._abortDialog}
.disabled=${this._loading}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
.disabled=${!this._domain || !this._clientId || !this._clientSecret}
@click=${this._addApplicationCredential}
.loading=${this._loading}
>
${this.hass.localize(
"ui.panel.config.application_credentials.editor.add"
)}
</ha-button>
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this._abortDialog}
.disabled=${this._loading}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
.disabled=${!this._domain ||
!this._clientId ||
!this._clientSecret}
@click=${this._addApplicationCredential}
.loading=${this._loading}
>
${this.hass.localize(
"ui.panel.config.application_credentials.editor.add"
)}
</ha-button>`}
</ha-dialog>
`;
}
@@ -341,6 +357,11 @@ export class DialogAddApplicationCredential extends LitElement {
ha-markdown {
margin-bottom: 16px;
}
ha-fade-in {
display: flex;
width: 100%;
justify-content: center;
}
`,
];
}

View File

@@ -1,3 +1,4 @@
import "@material/mwc-button";
import { mdiHelpCircle, mdiStarFourPoints } from "@mdi/js";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";

View File

@@ -0,0 +1,225 @@
import { mdiStop, mdiValveClosed, mdiValveOpen } from "@mdi/js";
import { html, LitElement, nothing, css } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-svg-icon";
import {
canClose,
canOpen,
canStop,
ValveEntityFeature,
type ValveEntity,
} from "../../../data/valve";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity";
import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
ValveOpenCloseCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
import "../../../components/ha-control-switch";
export const supportsValveOpenCloseCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "valve" &&
(supportsFeature(stateObj, ValveEntityFeature.OPEN) ||
supportsFeature(stateObj, ValveEntityFeature.CLOSE))
);
};
@customElement("hui-valve-open-close-card-feature")
class HuiValveOpenCloseCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: ValveOpenCloseCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as ValveEntity | undefined;
}
static getStubConfig(): ValveOpenCloseCardFeatureConfig {
return {
type: "valve-open-close",
};
}
public setConfig(config: ValveOpenCloseCardFeatureConfig): void {
if (!config) {
throw new Error("Invalid configuration");
}
this._config = config;
}
private _onOpenValve(): void {
this.hass!.callService("valve", "open_valve", {
entity_id: this._stateObj!.entity_id,
});
}
private _onCloseValve(): void {
this.hass!.callService("valve", "close_valve", {
entity_id: this._stateObj!.entity_id,
});
}
private _onOpenTap(ev): void {
ev.stopPropagation();
this._onOpenValve();
}
private _onCloseTap(ev): void {
ev.stopPropagation();
this._onCloseValve();
}
private _onStopTap(ev): void {
ev.stopPropagation();
this.hass!.callService("valve", "stop_valve", {
entity_id: this._stateObj!.entity_id,
});
}
private _valueChanged(ev): void {
ev.stopPropagation();
const checked = ev.target.checked as boolean;
if (checked) {
this._onOpenValve();
} else {
this._onCloseValve();
}
}
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsValveOpenCloseCardFeature(this.hass, this.context)
) {
return nothing;
}
// Determine colors and active states for toggle-style UI
const openColor = stateColorCss(this._stateObj, "open");
const closedColor = stateColorCss(this._stateObj, "closed");
const openIcon = mdiValveOpen;
const closedIcon = mdiValveClosed;
const isOpen =
this._stateObj.state === "open" ||
this._stateObj.state === "closing" ||
this._stateObj.state === "opening";
const isClosed = this._stateObj.state === "closed";
if (
this._stateObj.attributes.assumed_state ||
this._stateObj.state === UNKNOWN
) {
return html`
<ha-control-button-group>
${supportsFeature(this._stateObj, ValveEntityFeature.CLOSE)
? html`
<ha-control-button
.label=${this.hass.localize("ui.card.valve.close_valve")}
@click=${this._onCloseTap}
.disabled=${!canClose(this._stateObj)}
class=${classMap({
active: isClosed,
})}
style=${styleMap({
"--color": closedColor,
})}
>
<ha-svg-icon .path=${mdiValveClosed}></ha-svg-icon>
</ha-control-button>
`
: nothing}
${supportsFeature(this._stateObj, ValveEntityFeature.STOP)
? html`
<ha-control-button
.label=${this.hass.localize("ui.card.valve.stop_valve")}
@click=${this._onStopTap}
.disabled=${!canStop(this._stateObj)}
>
<ha-svg-icon .path=${mdiStop}></ha-svg-icon>
</ha-control-button>
`
: nothing}
${supportsFeature(this._stateObj, ValveEntityFeature.OPEN)
? html`
<ha-control-button
.label=${this.hass.localize("ui.card.valve.open_valve")}
@click=${this._onOpenTap}
.disabled=${!canOpen(this._stateObj)}
class=${classMap({
active: isOpen,
})}
style=${styleMap({
"--color": openColor,
})}
>
<ha-svg-icon .path=${mdiValveOpen}></ha-svg-icon>
</ha-control-button>
`
: nothing}
</ha-control-button-group>
`;
}
return html`
<ha-control-switch
.pathOn=${openIcon}
.pathOff=${closedIcon}
.checked=${isOpen}
@change=${this._valueChanged}
.label=${this.hass.localize("ui.card.common.toggle")}
.disabled=${this._stateObj.state === UNAVAILABLE}
>
</ha-control-switch>
`;
}
static get styles() {
return [
cardFeatureStyles,
css`
ha-control-button.active {
--control-button-icon-color: white;
--control-button-background-color: var(--color);
--control-button-background-opacity: 1;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-valve-open-close-card-feature": HuiValveOpenCloseCardFeature;
}
}

View File

@@ -153,6 +153,10 @@ export interface VacuumCommandsCardFeatureConfig {
commands?: VacuumCommand[];
}
export interface ValveOpenCloseCardFeatureConfig {
type: "valve-open-close";
}
export const LAWN_MOWER_COMMANDS = ["start_pause", "dock"] as const;
export type LawnMowerCommand = (typeof LAWN_MOWER_COMMANDS)[number];
@@ -223,6 +227,7 @@ export type LovelaceCardFeatureConfig =
| ToggleCardFeatureConfig
| UpdateActionsCardFeatureConfig
| VacuumCommandsCardFeatureConfig
| ValveOpenCloseCardFeatureConfig
| WaterHeaterOperationModesCardFeatureConfig
| AreaControlsCardFeatureConfig;

View File

@@ -6,6 +6,7 @@ import "../../../../components/ha-card";
import "../../../../components/ha-svg-icon";
import type { EnergyData } from "../../../../data/energy";
import {
computeConsumptionData,
energySourcesByType,
getEnergyDataCollection,
getSummedData,
@@ -92,6 +93,10 @@ class HuiEnergySankeyCard
const prefs = this._data.prefs;
const types = energySourcesByType(prefs);
const { summedData, compareSummedData: _ } = getSummedData(this._data);
const { consumption, compareConsumption: __ } = computeConsumptionData(
summedData,
undefined
);
const computedStyle = getComputedStyle(this);
@@ -103,12 +108,60 @@ class HuiEnergySankeyCard
label: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_distribution.home"
),
value: 0,
value: Math.max(0, consumption.total.used_total),
color: computedStyle.getPropertyValue("--primary-color"),
index: 1,
};
nodes.push(homeNode);
if (types.battery) {
const totalBatteryOut = summedData.total.from_battery ?? 0;
const totalBatteryIn = summedData.total.to_battery ?? 0;
// Add battery source
nodes.push({
id: "battery",
label: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_distribution.battery"
),
value: totalBatteryOut,
tooltip: `${formatNumber(totalBatteryOut, this.hass.locale)} kWh`,
color: computedStyle.getPropertyValue("--energy-battery-out-color"),
index: 0,
});
links.push({
source: "battery",
target: "home",
value: consumption.total.used_battery,
});
// Add battery sink
nodes.push({
id: "battery_in",
label: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_distribution.battery"
),
value: totalBatteryIn,
tooltip: `${formatNumber(totalBatteryIn, this.hass.locale)} kWh`,
color: computedStyle.getPropertyValue("--energy-battery-in-color"),
index: 1,
});
if (consumption.total.grid_to_battery > 0) {
links.push({
source: "grid",
target: "battery_in",
value: consumption.total.grid_to_battery,
});
}
if (consumption.total.solar_to_battery > 0) {
links.push({
source: "solar",
target: "battery_in",
value: consumption.total.solar_to_battery,
});
}
}
if (types.grid) {
const totalFromGrid = summedData.total.from_grid ?? 0;
@@ -128,6 +181,7 @@ class HuiEnergySankeyCard
links.push({
source: "grid",
target: "home",
value: consumption.total.used_grid,
});
}
@@ -149,57 +203,7 @@ class HuiEnergySankeyCard
links.push({
source: "solar",
target: "home",
});
}
// Calculate total home consumption from all producers
homeNode.value = nodes
.filter((node) => node.index === 0)
.reduce((sum, node) => sum + (node.value || 0), 0);
if (types.battery) {
// Add battery source
const totalBatteryOut = summedData.total.from_battery ?? 0;
const totalBatteryIn = summedData.total.to_battery ?? 0;
const netBattery = totalBatteryOut - totalBatteryIn;
const netBatteryOut = Math.max(netBattery, 0);
const netBatteryIn = Math.max(-netBattery, 0);
homeNode.value += netBattery;
nodes.push({
id: "battery",
label: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_distribution.battery"
),
value: netBatteryOut,
tooltip: `${formatNumber(netBatteryOut, this.hass.locale)} kWh`,
color: computedStyle.getPropertyValue("--energy-battery-out-color"),
index: 0,
});
links.push({
source: "battery",
target: "home",
});
// Add battery sink
nodes.push({
id: "battery_in",
label: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_distribution.battery"
),
value: netBatteryIn,
tooltip: `${formatNumber(netBatteryIn, this.hass.locale)} kWh`,
color: computedStyle.getPropertyValue("--energy-battery-in-color"),
index: 1,
});
nodes.forEach((node) => {
// Link all sources to battery_in
if (node.index === 0) {
links.push({
source: node.id,
target: "battery_in",
});
}
value: consumption.total.used_solar,
});
}
@@ -217,17 +221,20 @@ class HuiEnergySankeyCard
color: computedStyle.getPropertyValue("--energy-grid-return-color"),
index: 1,
});
nodes.forEach((node) => {
// Link all non-grid sources to grid_return
if (node.index === 0 && node.id !== "grid") {
links.push({
source: node.id,
target: "grid_return",
});
}
});
homeNode.value -= totalToGrid;
if (consumption.total.battery_to_grid > 0) {
links.push({
source: "battery",
target: "grid",
value: consumption.total.battery_to_grid,
});
}
if (consumption.total.solar_to_grid > 0) {
links.push({
source: "solar",
target: "grid_return",
value: consumption.total.solar_to_grid,
});
}
}
let untrackedConsumption = homeNode.value;
@@ -370,9 +377,6 @@ class HuiEnergySankeyCard
target: "untracked",
value: untrackedConsumption,
});
} else if (untrackedConsumption < 0) {
// if untracked consumption is negative, then the sources are not enough
homeNode.value -= untrackedConsumption;
}
homeNode.tooltip = `${formatNumber(homeNode.value, this.hass.locale)} kWh`;

View File

@@ -28,6 +28,7 @@ import "../card-features/hui-target-temperature-card-feature";
import "../card-features/hui-toggle-card-feature";
import "../card-features/hui-update-actions-card-feature";
import "../card-features/hui-vacuum-commands-card-feature";
import "../card-features/hui-valve-open-close-card-feature";
import "../card-features/hui-water-heater-operation-modes-card-feature";
import "../card-features/hui-area-controls-card-feature";
@@ -69,6 +70,7 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
"toggle",
"update-actions",
"vacuum-commands",
"valve-open-close",
"water-heater-operation-modes",
]);

View File

@@ -48,6 +48,7 @@ import { supportsTargetTemperatureCardFeature } from "../../card-features/hui-ta
import { supportsToggleCardFeature } from "../../card-features/hui-toggle-card-feature";
import { supportsUpdateActionsCardFeature } from "../../card-features/hui-update-actions-card-feature";
import { supportsVacuumCommandsCardFeature } from "../../card-features/hui-vacuum-commands-card-feature";
import { supportsValveOpenCloseCardFeature } from "../../card-features/hui-valve-open-close-card-feature";
import { supportsWaterHeaterOperationModesCardFeature } from "../../card-features/hui-water-heater-operation-modes-card-feature";
import type {
LovelaceCardFeatureConfig,
@@ -94,6 +95,7 @@ const UI_FEATURE_TYPES = [
"toggle",
"update-actions",
"vacuum-commands",
"valve-open-close",
"water-heater-operation-modes",
] as const satisfies readonly FeatureType[];
@@ -155,6 +157,7 @@ const SUPPORTS_FEATURE_TYPES: Record<
toggle: supportsToggleCardFeature,
"update-actions": supportsUpdateActionsCardFeature,
"vacuum-commands": supportsVacuumCommandsCardFeature,
"valve-open-close": supportsValveOpenCloseCardFeature,
"water-heater-operation-modes": supportsWaterHeaterOperationModesCardFeature,
};

View File

@@ -150,6 +150,10 @@ class LovelaceFullConfigEditor extends LitElement {
font-size: var(--ha-font-size-l);
}
ha-code-editor {
height: 100%;
}
.save-button {
opacity: 0;
font-size: var(--ha-font-size-m);

View File

@@ -1168,12 +1168,6 @@
},
"summary": "Summary",
"description": "Description"
},
"views": {
"dayGridMonth": "[%key:ui::panel::lovelace::editor::card::calendar::views::dayGridMonth%]",
"dayGridWeek": "[%key:ui::panel::lovelace::editor::card::calendar::views::dayGridWeek%]",
"dayGridDay": "[%key:ui::panel::lovelace::editor::card::calendar::views::dayGridDay%]",
"listWeek": "[%key:ui::panel::lovelace::editor::card::calendar::views::listWeek%]"
}
},
"attributes": {
@@ -7891,6 +7885,9 @@
"return_home": "[%key:ui::dialogs::more_info_control::vacuum::return_home%]"
}
},
"valve-open-close": {
"label": "Valve open/close"
},
"climate-fan-modes": {
"label": "Climate fan modes",
"style": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style%]",

View File

@@ -1848,60 +1848,60 @@ __metadata:
languageName: node
linkType: hard
"@fullcalendar/core@npm:6.1.18":
version: 6.1.18
resolution: "@fullcalendar/core@npm:6.1.18"
"@fullcalendar/core@npm:6.1.19":
version: 6.1.19
resolution: "@fullcalendar/core@npm:6.1.19"
dependencies:
preact: "npm:~10.12.1"
checksum: 10/0e29608599f1b4f42c0e6381a29739a1aafb226e37f085ba0405ee806c2c1417fa3dc013b375749e1a284eb94e4949b9573990816145874cc10aaaaacc7d8756
checksum: 10/5584da8eb2196085f7bd1a7cf37e2654d8960dd14e0eceddc8a41029b4c19fbcdb993cc517d31587991c095611bddc9d29468be6fb73fd53a92c4e6bfe935bac
languageName: node
linkType: hard
"@fullcalendar/daygrid@npm:6.1.18":
version: 6.1.18
resolution: "@fullcalendar/daygrid@npm:6.1.18"
"@fullcalendar/daygrid@npm:6.1.19":
version: 6.1.19
resolution: "@fullcalendar/daygrid@npm:6.1.19"
peerDependencies:
"@fullcalendar/core": ~6.1.18
checksum: 10/a91dc05445b7ad9210fb964d0e3bd378c74660a9f4d32ca3b28d84a79060795335ca8fd3b01277513e912eaeac37156d50976df281abf78a8ec5c79b5c93952e
"@fullcalendar/core": ~6.1.19
checksum: 10/1d1f15685e53fe73713ed523a497d9d5c8660d20e08c50a0c4bf040902144c9e571f536932a6b5c9ccc60c00fe2127879963bf05fbda736eaced439ddc06a1b3
languageName: node
linkType: hard
"@fullcalendar/interaction@npm:6.1.18":
version: 6.1.18
resolution: "@fullcalendar/interaction@npm:6.1.18"
"@fullcalendar/interaction@npm:6.1.19":
version: 6.1.19
resolution: "@fullcalendar/interaction@npm:6.1.19"
peerDependencies:
"@fullcalendar/core": ~6.1.18
checksum: 10/6958abef8a3a677c10fb8900f744019c35a7afe62dd0f031f9cb91812303cd2b3bbca6eb1443573a4d9e37a25f64472668e38247ffbed980416245867c8689f8
"@fullcalendar/core": ~6.1.19
checksum: 10/a9df02f92301548036603a291a03fe3383baab4b17eeb4db59f89cb18a605c70fb6710f3fcfb9391e899ca76eea384a6686928bd1d848e503c1dc8558c0b688e
languageName: node
linkType: hard
"@fullcalendar/list@npm:6.1.18":
version: 6.1.18
resolution: "@fullcalendar/list@npm:6.1.18"
"@fullcalendar/list@npm:6.1.19":
version: 6.1.19
resolution: "@fullcalendar/list@npm:6.1.19"
peerDependencies:
"@fullcalendar/core": ~6.1.18
checksum: 10/5d352a3b2311d9dda84a72ef5f3c990ede7f57f64ae9374629db8139bac26584748600b7af7686d39e7e88d10c16bfc60208fa9dd5727c8e343c6652e81a9ac0
"@fullcalendar/core": ~6.1.19
checksum: 10/c988707056122f55b8f20fda0f2b26d82e2ae96feb257e7537a7579aaed1ea600ee043ba11c4f5945a3d8ab231083978b5f89a8e2ee86d3ce61e65fdd126aca3
languageName: node
linkType: hard
"@fullcalendar/luxon3@npm:6.1.18":
version: 6.1.18
resolution: "@fullcalendar/luxon3@npm:6.1.18"
"@fullcalendar/luxon3@npm:6.1.19":
version: 6.1.19
resolution: "@fullcalendar/luxon3@npm:6.1.19"
peerDependencies:
"@fullcalendar/core": ~6.1.18
"@fullcalendar/core": ~6.1.19
luxon: ^3.0.0
checksum: 10/e7426e5dd4386977adf1469e5f52178e5e6b87d7c1931e8b928699db7c9a759c4e564971614f01d8463896e2c31767f2f104d7b3a0a74c08ca568396afeac2bd
checksum: 10/af834fc8bc2528be07baf3a1c15f2a6e09a662044545ad6838e305ff0098c85866ba83770912ea1d1a1db86b97ca7cd9f93d2517f6f4fb5914d711bd5c2e1078
languageName: node
linkType: hard
"@fullcalendar/timegrid@npm:6.1.18":
version: 6.1.18
resolution: "@fullcalendar/timegrid@npm:6.1.18"
"@fullcalendar/timegrid@npm:6.1.19":
version: 6.1.19
resolution: "@fullcalendar/timegrid@npm:6.1.19"
dependencies:
"@fullcalendar/daygrid": "npm:~6.1.18"
"@fullcalendar/daygrid": "npm:~6.1.19"
peerDependencies:
"@fullcalendar/core": ~6.1.18
checksum: 10/6eff8fff54fd5452fc47430bd52c28e70ee5eb58780de4a98671e290911ad0c265fa763452272c65e2bd304d7ac4aecb92d3c526b265f222ebb60274a366199f
"@fullcalendar/core": ~6.1.19
checksum: 10/8d8e1b7253528c592d1ce7ffa4c76f61480c725ffee55b9b2ec3bba156bf386250f27e3b6fff66073005e2c413efecc22fa97f1ec0bc762e4ae880633e641cb2
languageName: node
linkType: hard
@@ -2666,7 +2666,7 @@ __metadata:
languageName: node
linkType: hard
"@material/mwc-button@npm:^0.27.0":
"@material/mwc-button@npm:0.27.0, @material/mwc-button@npm:^0.27.0":
version: 0.27.0
resolution: "@material/mwc-button@npm:0.27.0"
dependencies:
@@ -9366,12 +9366,12 @@ __metadata:
"@formatjs/intl-numberformat": "npm:8.15.4"
"@formatjs/intl-pluralrules": "npm:5.4.4"
"@formatjs/intl-relativetimeformat": "npm:11.4.11"
"@fullcalendar/core": "npm:6.1.18"
"@fullcalendar/daygrid": "npm:6.1.18"
"@fullcalendar/interaction": "npm:6.1.18"
"@fullcalendar/list": "npm:6.1.18"
"@fullcalendar/luxon3": "npm:6.1.18"
"@fullcalendar/timegrid": "npm:6.1.18"
"@fullcalendar/core": "npm:6.1.19"
"@fullcalendar/daygrid": "npm:6.1.19"
"@fullcalendar/interaction": "npm:6.1.19"
"@fullcalendar/list": "npm:6.1.19"
"@fullcalendar/luxon3": "npm:6.1.19"
"@fullcalendar/timegrid": "npm:6.1.19"
"@lezer/highlight": "npm:1.2.1"
"@lit-labs/motion": "npm:1.0.9"
"@lit-labs/observers": "npm:2.0.6"
@@ -9382,6 +9382,7 @@ __metadata:
"@material/chips": "npm:=14.0.0-canary.53b3cad2f.0"
"@material/data-table": "npm:=14.0.0-canary.53b3cad2f.0"
"@material/mwc-base": "npm:0.27.0"
"@material/mwc-button": "npm:0.27.0"
"@material/mwc-checkbox": "npm:0.27.0"
"@material/mwc-dialog": "npm:0.27.0"
"@material/mwc-drawer": "npm:0.27.0"