mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-19 22:29:27 +00:00
Compare commits
7 Commits
feature-ba
...
graph-feat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de2be8fb11 | ||
|
|
152b39e188 | ||
|
|
83ecbbca88 | ||
|
|
8e7fbe4cf4 | ||
|
|
53f17fea08 | ||
|
|
7a64dd1d7e | ||
|
|
d7d1d9d5b6 |
2
.github/workflows/restrict-task-creation.yml
vendored
2
.github/workflows/restrict-task-creation.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
check-authorization:
|
check-authorization:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Only run if this is a Task issue type (from the issue form)
|
# Only run if this is a Task issue type (from the issue form)
|
||||||
if: github.event.issue.type.name == 'Task'
|
if: github.event.issue.issue_type == 'Task'
|
||||||
steps:
|
steps:
|
||||||
- name: Check if user is authorized
|
- name: Check if user is authorized
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
|
|||||||
@@ -14,5 +14,5 @@
|
|||||||
"name": "Home Assistant Cast",
|
"name": "Home Assistant Cast",
|
||||||
"short_name": "HA Cast",
|
"short_name": "HA Cast",
|
||||||
"start_url": "/?homescreen=1",
|
"start_url": "/?homescreen=1",
|
||||||
"theme_color": "#009ac7"
|
"theme_color": "#03A9F4"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,5 +75,5 @@
|
|||||||
"name": "Home Assistant Demo",
|
"name": "Home Assistant Demo",
|
||||||
"short_name": "HA Demo",
|
"short_name": "HA Demo",
|
||||||
"start_url": "/?homescreen=1",
|
"start_url": "/?homescreen=1",
|
||||||
"theme_color": "#009ac7"
|
"theme_color": "#03A9F4"
|
||||||
}
|
}
|
||||||
|
|||||||
23
package.json
23
package.json
@@ -27,7 +27,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@awesome.me/webawesome": "3.0.0-beta.4",
|
"@awesome.me/webawesome": "3.0.0-beta.4",
|
||||||
"@babel/runtime": "7.28.3",
|
"@babel/runtime": "7.28.2",
|
||||||
"@braintree/sanitize-url": "7.1.1",
|
"@braintree/sanitize-url": "7.1.1",
|
||||||
"@codemirror/autocomplete": "6.18.6",
|
"@codemirror/autocomplete": "6.18.6",
|
||||||
"@codemirror/commands": "6.8.1",
|
"@codemirror/commands": "6.8.1",
|
||||||
@@ -61,6 +61,7 @@
|
|||||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||||
"@material/mwc-base": "0.27.0",
|
"@material/mwc-base": "0.27.0",
|
||||||
|
"@material/mwc-button": "0.27.0",
|
||||||
"@material/mwc-checkbox": "0.27.0",
|
"@material/mwc-checkbox": "0.27.0",
|
||||||
"@material/mwc-dialog": "0.27.0",
|
"@material/mwc-dialog": "0.27.0",
|
||||||
"@material/mwc-drawer": "0.27.0",
|
"@material/mwc-drawer": "0.27.0",
|
||||||
@@ -89,8 +90,8 @@
|
|||||||
"@thomasloven/round-slider": "0.6.0",
|
"@thomasloven/round-slider": "0.6.0",
|
||||||
"@tsparticles/engine": "3.9.1",
|
"@tsparticles/engine": "3.9.1",
|
||||||
"@tsparticles/preset-links": "3.2.0",
|
"@tsparticles/preset-links": "3.2.0",
|
||||||
"@vaadin/combo-box": "24.8.5",
|
"@vaadin/combo-box": "24.7.9",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.8.5",
|
"@vaadin/vaadin-themable-mixin": "24.7.9",
|
||||||
"@vibrant/color": "4.0.0",
|
"@vibrant/color": "4.0.0",
|
||||||
"@vue/web-component-wrapper": "1.3.0",
|
"@vue/web-component-wrapper": "1.3.0",
|
||||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||||
@@ -112,7 +113,7 @@
|
|||||||
"fuse.js": "7.1.0",
|
"fuse.js": "7.1.0",
|
||||||
"google-timezones-json": "1.2.0",
|
"google-timezones-json": "1.2.0",
|
||||||
"gulp-zopfli-green": "6.0.2",
|
"gulp-zopfli-green": "6.0.2",
|
||||||
"hls.js": "1.6.10",
|
"hls.js": "1.6.9",
|
||||||
"home-assistant-js-websocket": "9.5.0",
|
"home-assistant-js-websocket": "9.5.0",
|
||||||
"idb-keyval": "6.2.2",
|
"idb-keyval": "6.2.2",
|
||||||
"intl-messageformat": "10.7.16",
|
"intl-messageformat": "10.7.16",
|
||||||
@@ -149,16 +150,16 @@
|
|||||||
"xss": "1.0.15"
|
"xss": "1.0.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.28.3",
|
"@babel/core": "7.28.0",
|
||||||
"@babel/helper-define-polyfill-provider": "0.6.5",
|
"@babel/helper-define-polyfill-provider": "0.6.5",
|
||||||
"@babel/plugin-transform-runtime": "7.28.3",
|
"@babel/plugin-transform-runtime": "7.28.0",
|
||||||
"@babel/preset-env": "7.28.3",
|
"@babel/preset-env": "7.28.0",
|
||||||
"@bundle-stats/plugin-webpack-filter": "4.21.2",
|
"@bundle-stats/plugin-webpack-filter": "4.21.2",
|
||||||
"@lokalise/node-api": "15.2.1",
|
"@lokalise/node-api": "15.0.0",
|
||||||
"@octokit/auth-oauth-device": "8.0.1",
|
"@octokit/auth-oauth-device": "8.0.1",
|
||||||
"@octokit/plugin-retry": "8.0.1",
|
"@octokit/plugin-retry": "8.0.1",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@rsdoctor/rspack-plugin": "1.2.2",
|
"@rsdoctor/rspack-plugin": "1.2.1",
|
||||||
"@rspack/cli": "1.4.11",
|
"@rspack/cli": "1.4.11",
|
||||||
"@rspack/core": "1.4.11",
|
"@rspack/core": "1.4.11",
|
||||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||||
@@ -218,7 +219,7 @@
|
|||||||
"terser-webpack-plugin": "5.3.14",
|
"terser-webpack-plugin": "5.3.14",
|
||||||
"ts-lit-plugin": "2.0.2",
|
"ts-lit-plugin": "2.0.2",
|
||||||
"typescript": "5.9.2",
|
"typescript": "5.9.2",
|
||||||
"typescript-eslint": "8.39.1",
|
"typescript-eslint": "8.39.0",
|
||||||
"vite-tsconfig-paths": "5.1.4",
|
"vite-tsconfig-paths": "5.1.4",
|
||||||
"vitest": "3.2.4",
|
"vitest": "3.2.4",
|
||||||
"webpack-stats-plugin": "1.1.3",
|
"webpack-stats-plugin": "1.1.3",
|
||||||
@@ -235,7 +236,7 @@
|
|||||||
"globals": "16.3.0",
|
"globals": "16.3.0",
|
||||||
"tslib": "2.8.1",
|
"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",
|
"@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",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.8.5"
|
"@vaadin/vaadin-themable-mixin": "24.7.9"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.9.2"
|
"packageManager": "yarn@4.9.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 type { TemplateResult } from "lit";
|
||||||
import { css, html, LitElement } 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 { fireEvent } from "../common/dom/fire_event";
|
||||||
import type { ToggleButton } from "../types";
|
import type { ToggleButton } from "../types";
|
||||||
import "./ha-svg-icon";
|
import "./ha-icon-button";
|
||||||
import "./ha-button";
|
|
||||||
import "./ha-button-group";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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")
|
@customElement("ha-button-toggle-group")
|
||||||
export class HaButtonToggleGroup extends LitElement {
|
export class HaButtonToggleGroup extends LitElement {
|
||||||
@property({ attribute: false }) public buttons!: ToggleButton[];
|
@property({ attribute: false }) public buttons!: ToggleButton[];
|
||||||
|
|
||||||
@property() public active?: string;
|
@property() public active?: string;
|
||||||
|
|
||||||
@property({ reflect: true }) size: "small" | "medium" = "medium";
|
@property({ attribute: "full-width", type: Boolean })
|
||||||
|
public fullWidth = false;
|
||||||
|
|
||||||
@property() public variant:
|
@property({ type: Boolean }) public dense = false;
|
||||||
| "brand"
|
|
||||||
| "neutral"
|
@queryAll("mwc-button") private _buttons?: Button[];
|
||||||
| "success"
|
|
||||||
| "warning"
|
|
||||||
| "danger" = "brand";
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<ha-button-group .variant=${this.variant} .size=${this.size}>
|
<div>
|
||||||
${this.buttons.map(
|
${this.buttons.map((button) =>
|
||||||
(button) =>
|
button.iconPath
|
||||||
html`<ha-button
|
? html`<ha-icon-button
|
||||||
class="icon"
|
.label=${button.label}
|
||||||
.value=${button.value}
|
.path=${button.iconPath}
|
||||||
@click=${this._handleClick}
|
.value=${button.value}
|
||||||
.title=${button.label}
|
?active=${this.active === button.value}
|
||||||
.appearance=${this.active === button.value ? "accent" : "filled"}
|
@click=${this._handleClick}
|
||||||
>
|
></ha-icon-button>`
|
||||||
${button.iconPath
|
: html`<mwc-button
|
||||||
? html`<ha-svg-icon
|
style=${styleMap({
|
||||||
aria-label=${button.label}
|
width: this.fullWidth
|
||||||
.path=${button.iconPath}
|
? `${100 / this.buttons.length}%`
|
||||||
></ha-svg-icon>`
|
: "initial",
|
||||||
: button.label}
|
})}
|
||||||
</ha-button>`
|
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 {
|
private _handleClick(ev): void {
|
||||||
this.active = ev.currentTarget.value;
|
this.active = ev.currentTarget.value;
|
||||||
fireEvent(this, "value-changed", { value: this.active });
|
fireEvent(this, "value-changed", { value: this.active });
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = css`
|
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);
|
--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;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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} loading - shows a loading indicator instead of the buttons label and disable buttons click.
|
||||||
* @attr {boolean} disabled - Disables the button and prevents user interaction.
|
* @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 {
|
export class HaButton extends Button {
|
||||||
variant: "brand" | "neutral" | "success" | "warning" | "danger" = "brand";
|
variant: "brand" | "neutral" | "success" | "warning" | "danger" = "brand";
|
||||||
|
|
||||||
@@ -47,42 +47,6 @@ export class HaButton extends Button {
|
|||||||
return internals;
|
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 (__DEV__ && this.isIconButton && !hasIconLabel) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn(
|
|
||||||
'Icon buttons must have a label for screen readers. Add <ha-svg-icon aria-label="..."> to remove this warning.',
|
|
||||||
this
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
Button.styles,
|
Button.styles,
|
||||||
@@ -217,11 +181,6 @@ export class HaButton extends Button {
|
|||||||
color: var(--wa-color-on-normal);
|
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"])
|
:host([appearance~="filled"])
|
||||||
.button:not(.disabled):not(.loading):active {
|
.button:not(.disabled):not(.loading):active {
|
||||||
background-color: var(--button-color-fill-normal-active);
|
background-color: var(--button-color-fill-normal-active);
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import { HaTextField } from "./ha-textfield";
|
|||||||
|
|
||||||
@customElement("ha-combo-box-textfield")
|
@customElement("ha-combo-box-textfield")
|
||||||
export class HaComboBoxTextField extends HaTextField {
|
export class HaComboBoxTextField extends HaTextField {
|
||||||
@property({ type: Boolean, attribute: "force-blank-value" })
|
@property({ type: Boolean, attribute: "disable-set-value" })
|
||||||
public forceBlankValue = false;
|
public disableSetValue = false;
|
||||||
|
|
||||||
protected willUpdate(changedProps: PropertyValues): void {
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
super.willUpdate(changedProps);
|
super.willUpdate(changedProps);
|
||||||
if (changedProps.has("value") || changedProps.has("forceBlankValue")) {
|
if (changedProps.has("value")) {
|
||||||
if (this.forceBlankValue && this.value) {
|
if (this.disableSetValue) {
|
||||||
this.value = "";
|
this.value = changedProps.get("value") as string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export class HaComboBox extends LitElement {
|
|||||||
|
|
||||||
@query("ha-combo-box-textfield", true) private _inputElement!: HaTextField;
|
@query("ha-combo-box-textfield", true) private _inputElement!: HaTextField;
|
||||||
|
|
||||||
@state({ type: Boolean }) private _forceBlankValue = false;
|
@state({ type: Boolean }) private _disableSetValue = false;
|
||||||
|
|
||||||
private _overlayMutationObserver?: MutationObserver;
|
private _overlayMutationObserver?: MutationObserver;
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ export class HaComboBox extends LitElement {
|
|||||||
></div>`}
|
></div>`}
|
||||||
.icon=${this.icon}
|
.icon=${this.icon}
|
||||||
.invalid=${this.invalid}
|
.invalid=${this.invalid}
|
||||||
.forceBlankValue=${this._forceBlankValue}
|
.disableSetValue=${this._disableSetValue}
|
||||||
>
|
>
|
||||||
<slot name="icon" slot="leadingIcon"></slot>
|
<slot name="icon" slot="leadingIcon"></slot>
|
||||||
</ha-combo-box-textfield>
|
</ha-combo-box-textfield>
|
||||||
@@ -270,10 +270,10 @@ export class HaComboBox extends LitElement {
|
|||||||
if (opened) {
|
if (opened) {
|
||||||
// Wait 100ms to be sure vaddin-combo-box-light already tried to set the value
|
// Wait 100ms to be sure vaddin-combo-box-light already tried to set the value
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this._forceBlankValue = false;
|
this._disableSetValue = false;
|
||||||
}, 100);
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
this._forceBlankValue = true;
|
this._disableSetValue = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import WaAnimation from "@awesome.me/webawesome/dist/components/animation/animation";
|
import SlAnimation from "@shoelace-style/shoelace/dist/components/animation/animation.component";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
|
|
||||||
@customElement("ha-fade-in")
|
@customElement("ha-fade-in")
|
||||||
export class HaFadeIn extends WaAnimation {
|
export class HaFadeIn extends SlAnimation {
|
||||||
@property() public name = "fadeIn";
|
@property() public name = "fadeIn";
|
||||||
|
|
||||||
@property() public fill: FillMode = "both";
|
@property() public fill: FillMode = "both";
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class MediaManageButton extends LitElement {
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<ha-button appearance="filled" size="small" @click=${this._manage}>
|
<ha-button appearance="plain" size="small" @click=${this._manage}>
|
||||||
<ha-svg-icon .path=${mdiFolderEdit} slot="start"></ha-svg-icon>
|
<ha-svg-icon .path=${mdiFolderEdit} slot="start"></ha-svg-icon>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.components.media-browser.file_management.manage"
|
"ui.components.media-browser.file_management.manage"
|
||||||
|
|||||||
@@ -385,22 +385,30 @@ export class HAFullCalendar extends LitElement {
|
|||||||
if (!this._viewButtons) {
|
if (!this._viewButtons) {
|
||||||
this._viewButtons = [
|
this._viewButtons = [
|
||||||
{
|
{
|
||||||
label: localize("ui.components.calendar.views.dayGridMonth"),
|
label: localize(
|
||||||
|
"ui.panel.lovelace.editor.card.calendar.views.dayGridMonth"
|
||||||
|
),
|
||||||
value: "dayGridMonth",
|
value: "dayGridMonth",
|
||||||
iconPath: mdiViewModule,
|
iconPath: mdiViewModule,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: localize("ui.components.calendar.views.dayGridWeek"),
|
label: localize(
|
||||||
|
"ui.panel.lovelace.editor.card.calendar.views.dayGridWeek"
|
||||||
|
),
|
||||||
value: "dayGridWeek",
|
value: "dayGridWeek",
|
||||||
iconPath: mdiViewWeek,
|
iconPath: mdiViewWeek,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: localize("ui.components.calendar.views.dayGridDay"),
|
label: localize(
|
||||||
|
"ui.panel.lovelace.editor.card.calendar.views.dayGridDay"
|
||||||
|
),
|
||||||
value: "dayGridDay",
|
value: "dayGridDay",
|
||||||
iconPath: mdiViewDay,
|
iconPath: mdiViewDay,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: localize("ui.components.calendar.views.listWeek"),
|
label: localize(
|
||||||
|
"ui.panel.lovelace.editor.card.calendar.views.listWeek"
|
||||||
|
),
|
||||||
value: "listWeek",
|
value: "listWeek",
|
||||||
iconPath: mdiViewAgenda,
|
iconPath: mdiViewAgenda,
|
||||||
},
|
},
|
||||||
@@ -485,6 +493,10 @@ export class HAFullCalendar extends LitElement {
|
|||||||
--mdc-icon-button-size: 32px;
|
--mdc-icon-button-size: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ha-button-toggle-group {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
ha-fab {
|
ha-fab {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 16px;
|
bottom: 16px;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import "@material/mwc-button";
|
||||||
import { mdiHelpCircle, mdiStarFourPoints } from "@mdi/js";
|
import { mdiHelpCircle, mdiStarFourPoints } from "@mdi/js";
|
||||||
import { css, html, LitElement } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
|||||||
@@ -46,16 +46,6 @@ const STRATEGIES = [
|
|||||||
description:
|
description:
|
||||||
"ui.panel.config.lovelace.dashboards.dialog_new.strategy.areas.description",
|
"ui.panel.config.lovelace.dashboards.dialog_new.strategy.areas.description",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: "overview",
|
|
||||||
images: {
|
|
||||||
light: "/static/images/dashboard-options/light/icon-dashboard-areas.svg",
|
|
||||||
dark: "/static/images/dashboard-options/dark/icon-dashboard-areas.svg",
|
|
||||||
},
|
|
||||||
name: "ui.panel.config.lovelace.dashboards.dialog_new.strategy.overview.title",
|
|
||||||
description:
|
|
||||||
"ui.panel.config.lovelace.dashboards.dialog_new.strategy.overview.description",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "map",
|
type: "map",
|
||||||
images: {
|
images: {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { LitElement, css, html, nothing } from "lit";
|
|||||||
import { mdiPencil, mdiDownload } from "@mdi/js";
|
import { mdiPencil, mdiDownload } from "@mdi/js";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import "../../components/ha-menu-button";
|
import "../../components/ha-menu-button";
|
||||||
import "../../components/ha-icon-button-arrow-prev";
|
|
||||||
import "../../components/ha-list-item";
|
import "../../components/ha-list-item";
|
||||||
import "../../components/ha-top-app-bar-fixed";
|
import "../../components/ha-top-app-bar-fixed";
|
||||||
import type { LovelaceConfig } from "../../data/lovelace/config/types";
|
import type { LovelaceConfig } from "../../data/lovelace/config/types";
|
||||||
@@ -50,8 +49,6 @@ class PanelEnergy extends LitElement {
|
|||||||
|
|
||||||
@state() private _lovelace?: Lovelace;
|
@state() private _lovelace?: Lovelace;
|
||||||
|
|
||||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
|
||||||
|
|
||||||
public willUpdate(changedProps: PropertyValues) {
|
public willUpdate(changedProps: PropertyValues) {
|
||||||
if (!this.hasUpdated) {
|
if (!this.hasUpdated) {
|
||||||
this.hass.loadFragmentTranslation("lovelace");
|
this.hass.loadFragmentTranslation("lovelace");
|
||||||
@@ -68,29 +65,15 @@ class PanelEnergy extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _back(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
history.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
${this._searchParms.has("historyBack")
|
<ha-menu-button
|
||||||
? html`
|
slot="navigationIcon"
|
||||||
<ha-icon-button-arrow-prev
|
.hass=${this.hass}
|
||||||
@click=${this._back}
|
.narrow=${this.narrow}
|
||||||
slot="navigationIcon"
|
></ha-menu-button>
|
||||||
></ha-icon-button-arrow-prev>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<ha-menu-button
|
|
||||||
slot="navigationIcon"
|
|
||||||
.hass=${this.hass}
|
|
||||||
.narrow=${this.narrow}
|
|
||||||
></ha-menu-button>
|
|
||||||
`}
|
|
||||||
${!this.narrow
|
${!this.narrow
|
||||||
? html`<div class="main-title">
|
? html`<div class="main-title">
|
||||||
${this.hass.localize("panel.energy")}
|
${this.hass.localize("panel.energy")}
|
||||||
|
|||||||
@@ -0,0 +1,301 @@
|
|||||||
|
import { css, html, LitElement, nothing, svg } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
|
import {
|
||||||
|
computeHistory,
|
||||||
|
subscribeHistoryStatesTimeWindow,
|
||||||
|
} from "../../../data/history";
|
||||||
|
import type {
|
||||||
|
HistoryResult,
|
||||||
|
LineChartUnit,
|
||||||
|
TimelineEntity,
|
||||||
|
} from "../../../data/history";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import type { LovelaceCardFeature } from "../types";
|
||||||
|
import type {
|
||||||
|
LovelaceCardFeatureContext,
|
||||||
|
HistoryChartCardFeatureConfig,
|
||||||
|
} from "./types";
|
||||||
|
import { getSensorNumericDeviceClasses } from "../../../data/sensor";
|
||||||
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
|
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||||
|
import { getGraphColorByIndex } from "../../../common/color/colors";
|
||||||
|
import { computeTimelineColor } from "../../../components/chart/timeline-color";
|
||||||
|
import { downSampleLineData } from "../../../components/chart/down-sample";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
|
||||||
|
export const supportsHistoryChartCardFeature = (
|
||||||
|
_hass: HomeAssistant,
|
||||||
|
context: LovelaceCardFeatureContext
|
||||||
|
) =>
|
||||||
|
!!context.entity_id &&
|
||||||
|
["sensor", "binary_sensor"].includes(computeDomain(context.entity_id));
|
||||||
|
|
||||||
|
@customElement("hui-history-chart-card-feature")
|
||||||
|
class HuiHistoryChartCardFeature
|
||||||
|
extends SubscribeMixin(LitElement)
|
||||||
|
implements LovelaceCardFeature
|
||||||
|
{
|
||||||
|
@property({ attribute: false, hasChanged: () => false })
|
||||||
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||||
|
|
||||||
|
@state() private _config?: HistoryChartCardFeatureConfig;
|
||||||
|
|
||||||
|
@state() private _stateHistory?: HistoryResult;
|
||||||
|
|
||||||
|
private _interval?: number;
|
||||||
|
|
||||||
|
static getStubConfig(): HistoryChartCardFeatureConfig {
|
||||||
|
return {
|
||||||
|
type: "history-chart",
|
||||||
|
hours_to_show: 24,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public setConfig(config: HistoryChartCardFeatureConfig): void {
|
||||||
|
if (!config) {
|
||||||
|
throw new Error("Invalid configuration");
|
||||||
|
}
|
||||||
|
this._config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
// redraw the graph every minute to update the time axis
|
||||||
|
clearInterval(this._interval);
|
||||||
|
this._interval = window.setInterval(() => this.requestUpdate(), 1000 * 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
clearInterval(this._interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected hassSubscribe() {
|
||||||
|
return [this._subscribeHistory()];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (
|
||||||
|
!this._config ||
|
||||||
|
!this.hass ||
|
||||||
|
!this.context ||
|
||||||
|
!this._stateHistory ||
|
||||||
|
!supportsHistoryChartCardFeature(this.hass, this.context)
|
||||||
|
) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = this._stateHistory.line[0];
|
||||||
|
const timeline = this._stateHistory.timeline[0];
|
||||||
|
const width = this.clientWidth;
|
||||||
|
const height = this.clientHeight;
|
||||||
|
if (line) {
|
||||||
|
const points = this._generateLinePoints(line);
|
||||||
|
const { paths, filledPaths } = this._getLinePaths(points);
|
||||||
|
const color = getGraphColorByIndex(0, this.style);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="line" @click=${this._handleClick}>
|
||||||
|
${svg`<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
||||||
|
${paths.map(
|
||||||
|
(path) =>
|
||||||
|
svg`<path d="${path}" stroke="${color}" stroke-width="1" stroke-linecap="round" fill="none" />`
|
||||||
|
)}
|
||||||
|
${filledPaths.map(
|
||||||
|
(path) =>
|
||||||
|
svg`<path d="${path}" stroke="none" stroke-linecap="round" fill="${color}" fill-opacity="0.2" />`
|
||||||
|
)}
|
||||||
|
</svg>`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
if (timeline) {
|
||||||
|
const ranges = this._generateTimelineRanges(timeline);
|
||||||
|
return html`
|
||||||
|
<div class="timeline" @click=${this._handleClick}>
|
||||||
|
${svg`<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
||||||
|
<g>
|
||||||
|
${ranges.map((r) => svg`<rect x="${r.startX}" y="0" width="${r.endX - r.startX}" height="${height}" fill="${r.color}" />`)}
|
||||||
|
</g>
|
||||||
|
</svg>`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleClick() {
|
||||||
|
// open more info dialog to show more detailed history
|
||||||
|
fireEvent(this, "hass-more-info", { entityId: this.context!.entity_id! });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _subscribeHistory(): Promise<() => Promise<void>> {
|
||||||
|
if (
|
||||||
|
!isComponentLoaded(this.hass!, "history") ||
|
||||||
|
!this.context?.entity_id ||
|
||||||
|
!this._config
|
||||||
|
) {
|
||||||
|
return () => Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { numeric_device_classes: sensorNumericDeviceClasses } =
|
||||||
|
await getSensorNumericDeviceClasses(this.hass!);
|
||||||
|
|
||||||
|
return subscribeHistoryStatesTimeWindow(
|
||||||
|
this.hass!,
|
||||||
|
(historyStates) => {
|
||||||
|
this._stateHistory = computeHistory(
|
||||||
|
this.hass!,
|
||||||
|
historyStates,
|
||||||
|
[this.context!.entity_id!],
|
||||||
|
this.hass!.localize,
|
||||||
|
sensorNumericDeviceClasses,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
},
|
||||||
|
this._config!.hours_to_show ?? 24,
|
||||||
|
[this.context!.entity_id!]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _generateLinePoints(line: LineChartUnit): { x: number; y: number }[] {
|
||||||
|
const width = this.clientWidth;
|
||||||
|
const height = this.clientHeight;
|
||||||
|
let minY = Number(line.data[0].states[0].state);
|
||||||
|
let maxY = Number(line.data[0].states[0].state);
|
||||||
|
const minX = line.data[0].states[0].last_changed;
|
||||||
|
const maxX = Date.now();
|
||||||
|
line.data[0].states.forEach((stateData) => {
|
||||||
|
const stateValue = Number(stateData.state);
|
||||||
|
if (stateValue < minY) {
|
||||||
|
minY = stateValue;
|
||||||
|
}
|
||||||
|
if (stateValue > maxY) {
|
||||||
|
maxY = stateValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const rangeY = maxY - minY || minY * 0.1;
|
||||||
|
const sampledData = downSampleLineData(
|
||||||
|
line.data[0].states.map((stateData) => [
|
||||||
|
stateData.last_changed,
|
||||||
|
Number(stateData.state),
|
||||||
|
]),
|
||||||
|
width,
|
||||||
|
minX,
|
||||||
|
maxX
|
||||||
|
);
|
||||||
|
// add margin to the min and max
|
||||||
|
minY -= rangeY * 0.1;
|
||||||
|
maxY += rangeY * 0.1;
|
||||||
|
const yDenom = maxY - minY || 1;
|
||||||
|
const xDenom = maxX - minX || 1;
|
||||||
|
const points = sampledData!.map((point) => {
|
||||||
|
const x = ((point![0] - minX) / xDenom) * width;
|
||||||
|
const y = height - ((Number(point![1]) - minY) / yDenom) * height;
|
||||||
|
return { x, y };
|
||||||
|
});
|
||||||
|
points.push({ x: width, y: points[points.length - 1].y });
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _generateTimelineRanges(timeline: TimelineEntity) {
|
||||||
|
if (timeline.data.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const computedStyles = getComputedStyle(this);
|
||||||
|
const width = this.clientWidth;
|
||||||
|
const minX = timeline.data[0].last_changed;
|
||||||
|
const maxX = Date.now();
|
||||||
|
let prevEndX = 0;
|
||||||
|
let prevStateColor = "";
|
||||||
|
const ranges = timeline.data.map((t) => {
|
||||||
|
const x = ((t.last_changed - minX) / (maxX - minX)) * width;
|
||||||
|
const range = {
|
||||||
|
startX: prevEndX,
|
||||||
|
endX: x,
|
||||||
|
color: prevStateColor,
|
||||||
|
};
|
||||||
|
prevStateColor = computeTimelineColor(
|
||||||
|
t.state,
|
||||||
|
computedStyles,
|
||||||
|
this.hass!.states[timeline.entity_id]
|
||||||
|
);
|
||||||
|
prevEndX = x;
|
||||||
|
return range;
|
||||||
|
});
|
||||||
|
ranges.push({
|
||||||
|
startX: prevEndX,
|
||||||
|
endX: width,
|
||||||
|
color: prevStateColor,
|
||||||
|
});
|
||||||
|
return ranges;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getLinePaths(points: { x: number; y: number }[]) {
|
||||||
|
const paths: string[] = [];
|
||||||
|
const filledPaths: string[] = [];
|
||||||
|
if (!points.length) {
|
||||||
|
return { paths, filledPaths };
|
||||||
|
}
|
||||||
|
// path can interupted by missing data, so we need to split the path into segments
|
||||||
|
const pathSegments: { x: number; y: number }[][] = [[]];
|
||||||
|
points.forEach((point) => {
|
||||||
|
if (!isNaN(point.y)) {
|
||||||
|
pathSegments[pathSegments.length - 1].push(point);
|
||||||
|
} else if (pathSegments[pathSegments.length - 1].length > 0) {
|
||||||
|
pathSegments.push([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pathSegments.forEach((pathPoints) => {
|
||||||
|
// create a smoothed path
|
||||||
|
let next: { x: number; y: number };
|
||||||
|
let path = "";
|
||||||
|
let last = pathPoints[0];
|
||||||
|
|
||||||
|
path += `M ${last.x},${last.y}`;
|
||||||
|
|
||||||
|
pathPoints.forEach((coord) => {
|
||||||
|
next = coord;
|
||||||
|
path += ` ${(next.x + last.x) / 2},${(next.y + last.y) / 2}`;
|
||||||
|
path += ` Q${next.x},${next.y}`;
|
||||||
|
last = next;
|
||||||
|
});
|
||||||
|
|
||||||
|
path += ` ${next!.x},${next!.y}`;
|
||||||
|
paths.push(path);
|
||||||
|
filledPaths.push(
|
||||||
|
path +
|
||||||
|
` L ${next!.x},${this.clientHeight} L ${pathPoints[0].x},${this.clientHeight} Z`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { paths, filledPaths };
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--feature-height);
|
||||||
|
}
|
||||||
|
:host > div {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.timeline {
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-history-chart-card-feature": HuiHistoryChartCardFeature;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import { css, LitElement, nothing, html } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
|
||||||
import type { HomeAssistant } from "../../../types";
|
|
||||||
import type { LovelaceCardFeature } from "../types";
|
|
||||||
import type {
|
|
||||||
LovelaceCardFeatureContext,
|
|
||||||
ProgressBarCardFeatureConfig,
|
|
||||||
} from "./types";
|
|
||||||
|
|
||||||
export const supportsProgressBarCardFeature = (
|
|
||||||
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 === "sensor" && stateObj.attributes.unit_of_measurement === "%";
|
|
||||||
};
|
|
||||||
|
|
||||||
@customElement("hui-progress-bar-card-feature")
|
|
||||||
class HuiProgressBarCardFeature
|
|
||||||
extends LitElement
|
|
||||||
implements LovelaceCardFeature
|
|
||||||
{
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public context!: LovelaceCardFeatureContext;
|
|
||||||
|
|
||||||
@state() private _config?: ProgressBarCardFeatureConfig;
|
|
||||||
|
|
||||||
static getStubConfig(): ProgressBarCardFeatureConfig {
|
|
||||||
return {
|
|
||||||
type: "progress-bar",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public setConfig(config: ProgressBarCardFeatureConfig): void {
|
|
||||||
if (!config) {
|
|
||||||
throw new Error("Invalid configuration");
|
|
||||||
}
|
|
||||||
this._config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (
|
|
||||||
!this._config ||
|
|
||||||
!this.hass ||
|
|
||||||
!this.context ||
|
|
||||||
!this.context.entity_id ||
|
|
||||||
!this.hass.states[this.context.entity_id] ||
|
|
||||||
!supportsProgressBarCardFeature(this.hass, this.context)
|
|
||||||
) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
const stateObj = this.hass.states[this.context.entity_id];
|
|
||||||
const value = stateObj.state;
|
|
||||||
return html`<div style="width: ${value}%"></div>
|
|
||||||
<div class="progress-bar-background"></div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = css`
|
|
||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
height: var(--feature-height);
|
|
||||||
border-radius: var(--feature-border-radius);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
:host > div {
|
|
||||||
height: 100%;
|
|
||||||
background-color: var(--feature-color);
|
|
||||||
}
|
|
||||||
.progress-bar-background {
|
|
||||||
flex: 1;
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"hui-progress-bar-card-feature": HuiProgressBarCardFeature;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -175,6 +175,11 @@ export interface UpdateActionsCardFeatureConfig {
|
|||||||
backup?: "yes" | "no" | "ask";
|
backup?: "yes" | "no" | "ask";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HistoryChartCardFeatureConfig {
|
||||||
|
type: "history-chart";
|
||||||
|
hours_to_show: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const AREA_CONTROLS = [
|
export const AREA_CONTROLS = [
|
||||||
"light",
|
"light",
|
||||||
"fan",
|
"fan",
|
||||||
@@ -198,10 +203,6 @@ export interface AreaControlsCardFeatureConfig {
|
|||||||
controls?: AreaControl[];
|
controls?: AreaControl[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProgressBarCardFeatureConfig {
|
|
||||||
type: "progress-bar";
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LovelaceCardFeaturePosition = "bottom" | "inline";
|
export type LovelaceCardFeaturePosition = "bottom" | "inline";
|
||||||
|
|
||||||
export type LovelaceCardFeatureConfig =
|
export type LovelaceCardFeatureConfig =
|
||||||
@@ -230,6 +231,7 @@ export type LovelaceCardFeatureConfig =
|
|||||||
| MediaPlayerVolumeSliderCardFeatureConfig
|
| MediaPlayerVolumeSliderCardFeatureConfig
|
||||||
| NumericInputCardFeatureConfig
|
| NumericInputCardFeatureConfig
|
||||||
| SelectOptionsCardFeatureConfig
|
| SelectOptionsCardFeatureConfig
|
||||||
|
| HistoryChartCardFeatureConfig
|
||||||
| TargetHumidityCardFeatureConfig
|
| TargetHumidityCardFeatureConfig
|
||||||
| TargetTemperatureCardFeatureConfig
|
| TargetTemperatureCardFeatureConfig
|
||||||
| ToggleCardFeatureConfig
|
| ToggleCardFeatureConfig
|
||||||
@@ -238,8 +240,7 @@ export type LovelaceCardFeatureConfig =
|
|||||||
| ValveOpenCloseCardFeatureConfig
|
| ValveOpenCloseCardFeatureConfig
|
||||||
| ValvePositionCardFeatureConfig
|
| ValvePositionCardFeatureConfig
|
||||||
| WaterHeaterOperationModesCardFeatureConfig
|
| WaterHeaterOperationModesCardFeatureConfig
|
||||||
| AreaControlsCardFeatureConfig
|
| AreaControlsCardFeatureConfig;
|
||||||
| ProgressBarCardFeatureConfig;
|
|
||||||
|
|
||||||
export interface LovelaceCardFeatureContext {
|
export interface LovelaceCardFeatureContext {
|
||||||
entity_id?: string;
|
entity_id?: string;
|
||||||
|
|||||||
@@ -54,9 +54,6 @@ import { createEntityNotFoundWarning } from "../components/hui-warning";
|
|||||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||||
import type { TodoListCardConfig } from "./types";
|
import type { TodoListCardConfig } from "./types";
|
||||||
|
|
||||||
export const ITEM_TAP_ACTION_EDIT = "edit";
|
|
||||||
export const ITEM_TAP_ACTION_TOGGLE = "toggle";
|
|
||||||
|
|
||||||
@customElement("hui-todo-list-card")
|
@customElement("hui-todo-list-card")
|
||||||
export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||||
@@ -485,7 +482,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
|||||||
)}
|
)}
|
||||||
.itemId=${item.uid}
|
.itemId=${item.uid}
|
||||||
@change=${this._completeItem}
|
@change=${this._completeItem}
|
||||||
@click=${this._itemTap}
|
@click=${this._openItem}
|
||||||
@request-selected=${this._requestSelected}
|
@request-selected=${this._requestSelected}
|
||||||
@keydown=${this._handleKeydown}
|
@keydown=${this._handleKeydown}
|
||||||
>
|
>
|
||||||
@@ -578,18 +575,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ev.key === "Enter") {
|
if (ev.key === "Enter") {
|
||||||
this._itemTap(ev);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _itemTap(ev): void {
|
|
||||||
if (
|
|
||||||
!this._config!.item_tap_action ||
|
|
||||||
this._config!.item_tap_action === ITEM_TAP_ACTION_EDIT
|
|
||||||
) {
|
|
||||||
this._openItem(ev);
|
this._openItem(ev);
|
||||||
} else if (this._config!.item_tap_action === ITEM_TAP_ACTION_TOGGLE) {
|
|
||||||
this._completeItem(ev);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ import "../card-features/hui-valve-open-close-card-feature";
|
|||||||
import "../card-features/hui-valve-position-card-feature";
|
import "../card-features/hui-valve-position-card-feature";
|
||||||
import "../card-features/hui-water-heater-operation-modes-card-feature";
|
import "../card-features/hui-water-heater-operation-modes-card-feature";
|
||||||
import "../card-features/hui-area-controls-card-feature";
|
import "../card-features/hui-area-controls-card-feature";
|
||||||
import "../card-features/hui-progress-bar-card-feature";
|
import "../card-features/hui-history-chart-card-feature";
|
||||||
|
|
||||||
import type { LovelaceCardFeatureConfig } from "../card-features/types";
|
import type { LovelaceCardFeatureConfig } from "../card-features/types";
|
||||||
import {
|
import {
|
||||||
createLovelaceElement,
|
createLovelaceElement,
|
||||||
@@ -65,8 +66,8 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
|
|||||||
"lock-open-door",
|
"lock-open-door",
|
||||||
"media-player-volume-slider",
|
"media-player-volume-slider",
|
||||||
"numeric-input",
|
"numeric-input",
|
||||||
"progress-bar",
|
|
||||||
"select-options",
|
"select-options",
|
||||||
|
"history-chart",
|
||||||
"target-humidity",
|
"target-humidity",
|
||||||
"target-temperature",
|
"target-temperature",
|
||||||
"toggle",
|
"toggle",
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import { supportsLockOpenDoorCardFeature } from "../../card-features/hui-lock-op
|
|||||||
import { supportsMediaPlayerVolumeSliderCardFeature } from "../../card-features/hui-media-player-volume-slider-card-feature";
|
import { supportsMediaPlayerVolumeSliderCardFeature } from "../../card-features/hui-media-player-volume-slider-card-feature";
|
||||||
import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric-input-card-feature";
|
import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric-input-card-feature";
|
||||||
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
|
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
|
||||||
|
import { supportsHistoryChartCardFeature } from "../../card-features/hui-history-chart-card-feature";
|
||||||
import { supportsTargetHumidityCardFeature } from "../../card-features/hui-target-humidity-card-feature";
|
import { supportsTargetHumidityCardFeature } from "../../card-features/hui-target-humidity-card-feature";
|
||||||
import { supportsTargetTemperatureCardFeature } from "../../card-features/hui-target-temperature-card-feature";
|
import { supportsTargetTemperatureCardFeature } from "../../card-features/hui-target-temperature-card-feature";
|
||||||
import { supportsToggleCardFeature } from "../../card-features/hui-toggle-card-feature";
|
import { supportsToggleCardFeature } from "../../card-features/hui-toggle-card-feature";
|
||||||
@@ -50,7 +51,6 @@ import { supportsUpdateActionsCardFeature } from "../../card-features/hui-update
|
|||||||
import { supportsVacuumCommandsCardFeature } from "../../card-features/hui-vacuum-commands-card-feature";
|
import { supportsVacuumCommandsCardFeature } from "../../card-features/hui-vacuum-commands-card-feature";
|
||||||
import { supportsValveOpenCloseCardFeature } from "../../card-features/hui-valve-open-close-card-feature";
|
import { supportsValveOpenCloseCardFeature } from "../../card-features/hui-valve-open-close-card-feature";
|
||||||
import { supportsValvePositionCardFeature } from "../../card-features/hui-valve-position-card-feature";
|
import { supportsValvePositionCardFeature } from "../../card-features/hui-valve-position-card-feature";
|
||||||
import { supportsProgressBarCardFeature } from "../../card-features/hui-progress-bar-card-feature";
|
|
||||||
import { supportsWaterHeaterOperationModesCardFeature } from "../../card-features/hui-water-heater-operation-modes-card-feature";
|
import { supportsWaterHeaterOperationModesCardFeature } from "../../card-features/hui-water-heater-operation-modes-card-feature";
|
||||||
import type {
|
import type {
|
||||||
LovelaceCardFeatureConfig,
|
LovelaceCardFeatureConfig,
|
||||||
@@ -92,7 +92,7 @@ const UI_FEATURE_TYPES = [
|
|||||||
"media-player-volume-slider",
|
"media-player-volume-slider",
|
||||||
"numeric-input",
|
"numeric-input",
|
||||||
"select-options",
|
"select-options",
|
||||||
"progress-bar",
|
"history-chart",
|
||||||
"target-humidity",
|
"target-humidity",
|
||||||
"target-temperature",
|
"target-temperature",
|
||||||
"toggle",
|
"toggle",
|
||||||
@@ -156,7 +156,7 @@ const SUPPORTS_FEATURE_TYPES: Record<
|
|||||||
"media-player-volume-slider": supportsMediaPlayerVolumeSliderCardFeature,
|
"media-player-volume-slider": supportsMediaPlayerVolumeSliderCardFeature,
|
||||||
"numeric-input": supportsNumericInputCardFeature,
|
"numeric-input": supportsNumericInputCardFeature,
|
||||||
"select-options": supportsSelectOptionsCardFeature,
|
"select-options": supportsSelectOptionsCardFeature,
|
||||||
"progress-bar": supportsProgressBarCardFeature,
|
"history-chart": supportsHistoryChartCardFeature,
|
||||||
"target-humidity": supportsTargetHumidityCardFeature,
|
"target-humidity": supportsTargetHumidityCardFeature,
|
||||||
"target-temperature": supportsTargetTemperatureCardFeature,
|
"target-temperature": supportsTargetTemperatureCardFeature,
|
||||||
toggle: supportsToggleCardFeature,
|
toggle: supportsToggleCardFeature,
|
||||||
|
|||||||
@@ -261,9 +261,6 @@ export class HuiConditionalCardEditor
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.card-options {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.gui-mode-button {
|
.gui-mode-button {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
margin-inline-end: auto;
|
margin-inline-end: auto;
|
||||||
|
|||||||
@@ -3,11 +3,6 @@ import { html, LitElement, nothing } from "lit";
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { assert, assign, boolean, object, optional, string } from "superstruct";
|
import { assert, assign, boolean, object, optional, string } from "superstruct";
|
||||||
import { mdiGestureTap } from "@mdi/js";
|
|
||||||
import {
|
|
||||||
ITEM_TAP_ACTION_EDIT,
|
|
||||||
ITEM_TAP_ACTION_TOGGLE,
|
|
||||||
} from "../../cards/hui-todo-list-card";
|
|
||||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import "../../../../components/ha-alert";
|
import "../../../../components/ha-alert";
|
||||||
@@ -22,8 +17,6 @@ import { configElementStyle } from "./config-elements-style";
|
|||||||
import { TodoListEntityFeature, TodoSortMode } from "../../../../data/todo";
|
import { TodoListEntityFeature, TodoSortMode } from "../../../../data/todo";
|
||||||
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||||
|
|
||||||
const ITEM_TAP_ACTIONS = [ITEM_TAP_ACTION_EDIT, ITEM_TAP_ACTION_TOGGLE];
|
|
||||||
|
|
||||||
const cardConfigStruct = assign(
|
const cardConfigStruct = assign(
|
||||||
baseLovelaceCardConfig,
|
baseLovelaceCardConfig,
|
||||||
object({
|
object({
|
||||||
@@ -33,7 +26,6 @@ const cardConfigStruct = assign(
|
|||||||
hide_completed: optional(boolean()),
|
hide_completed: optional(boolean()),
|
||||||
hide_create: optional(boolean()),
|
hide_create: optional(boolean()),
|
||||||
display_order: optional(string()),
|
display_order: optional(string()),
|
||||||
item_tap_action: optional(string()),
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -72,35 +64,11 @@ export class HuiTodoListEditor
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "interactions",
|
|
||||||
type: "expandable",
|
|
||||||
flatten: true,
|
|
||||||
iconPath: mdiGestureTap,
|
|
||||||
schema: [
|
|
||||||
{
|
|
||||||
name: "item_tap_action",
|
|
||||||
required: true,
|
|
||||||
selector: {
|
|
||||||
select: {
|
|
||||||
mode: "dropdown",
|
|
||||||
options: Object.values(ITEM_TAP_ACTIONS).map((action) => ({
|
|
||||||
value: action,
|
|
||||||
label: localize(
|
|
||||||
`ui.panel.lovelace.editor.card.todo-list.actions.${action}`
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
] as const
|
] as const
|
||||||
);
|
);
|
||||||
|
|
||||||
private _data = memoizeOne((config) => ({
|
private _data = memoizeOne((config) => ({
|
||||||
display_order: "none",
|
display_order: "none",
|
||||||
item_tap_action: "edit",
|
|
||||||
...config,
|
...config,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -138,10 +106,7 @@ export class HuiTodoListEditor
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _valueChanged(ev: CustomEvent): void {
|
private _valueChanged(ev: CustomEvent): void {
|
||||||
const config = { ...ev.detail.value };
|
const config = ev.detail.value;
|
||||||
if (config.item_tap_action === ITEM_TAP_ACTION_EDIT) {
|
|
||||||
delete config.item_tap_action;
|
|
||||||
}
|
|
||||||
fireEvent(this, "config-changed", { config });
|
fireEvent(this, "config-changed", { config });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +130,6 @@ export class HuiTodoListEditor
|
|||||||
case "hide_completed":
|
case "hide_completed":
|
||||||
case "hide_create":
|
case "hide_create":
|
||||||
case "display_order":
|
case "display_order":
|
||||||
case "item_tap_action":
|
|
||||||
return this.hass!.localize(
|
return this.hass!.localize(
|
||||||
`ui.panel.lovelace.editor.card.todo-list.${schema.name}`
|
`ui.panel.lovelace.editor.card.todo-list.${schema.name}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
|
|||||||
map: () => import("./map/map-dashboard-strategy"),
|
map: () => import("./map/map-dashboard-strategy"),
|
||||||
iframe: () => import("./iframe/iframe-dashboard-strategy"),
|
iframe: () => import("./iframe/iframe-dashboard-strategy"),
|
||||||
areas: () => import("./areas/areas-dashboard-strategy"),
|
areas: () => import("./areas/areas-dashboard-strategy"),
|
||||||
overview: () => import("./overview/overview-dashboard-strategy"),
|
|
||||||
},
|
},
|
||||||
view: {
|
view: {
|
||||||
"original-states": () =>
|
"original-states": () =>
|
||||||
@@ -43,10 +42,6 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
|
|||||||
iframe: () => import("./iframe/iframe-view-strategy"),
|
iframe: () => import("./iframe/iframe-view-strategy"),
|
||||||
area: () => import("./areas/area-view-strategy"),
|
area: () => import("./areas/area-view-strategy"),
|
||||||
"areas-overview": () => import("./areas/areas-overview-view-strategy"),
|
"areas-overview": () => import("./areas/areas-overview-view-strategy"),
|
||||||
"overview-home": () => import("./overview/overview-home-view-strategy"),
|
|
||||||
"overview-lights": () => import("./overview/overview-lights-view-strategy"),
|
|
||||||
"overview-covers": () => import("./overview/overview-covers-view-strategy"),
|
|
||||||
"overview-area": () => import("./overview/overview-area-view-strategy"),
|
|
||||||
},
|
},
|
||||||
section: {},
|
section: {},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
import { html, LitElement, nothing } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
|
||||||
import "../../../../../components/entity/ha-entities-picker";
|
|
||||||
import type { HomeAssistant } from "../../../../../types";
|
|
||||||
import type { LovelaceStrategyEditor } from "../../types";
|
|
||||||
import type { OverviewDashboardStrategyConfig } from "../overview-dashboard-strategy";
|
|
||||||
|
|
||||||
@customElement("hui-overview-dashboard-strategy-editor")
|
|
||||||
export class HuiOverviewDashboardStrategyEditor
|
|
||||||
extends LitElement
|
|
||||||
implements LovelaceStrategyEditor
|
|
||||||
{
|
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private _config?: OverviewDashboardStrategyConfig;
|
|
||||||
|
|
||||||
public setConfig(config: OverviewDashboardStrategyConfig): void {
|
|
||||||
this._config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
if (!this.hass || !this._config) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<ha-entities-picker
|
|
||||||
.hass=${this.hass}
|
|
||||||
.value=${this._config.favorite_entities || []}
|
|
||||||
label="Favorite entities"
|
|
||||||
placeholder="Add favorite entity"
|
|
||||||
reorder
|
|
||||||
allow-custom-entity
|
|
||||||
@value-changed=${this._valueChanged}
|
|
||||||
>
|
|
||||||
</ha-entities-picker>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _valueChanged(ev: CustomEvent): void {
|
|
||||||
ev.stopPropagation();
|
|
||||||
if (!this._config || !this.hass) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const favoriteEntities = ev.detail.value as string[];
|
|
||||||
|
|
||||||
const config: OverviewDashboardStrategyConfig = {
|
|
||||||
...this._config,
|
|
||||||
favorite_entities: favoriteEntities,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (config.favorite_entities?.length === 0) {
|
|
||||||
delete config.favorite_entities;
|
|
||||||
}
|
|
||||||
|
|
||||||
fireEvent(this, "config-changed", { config });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"hui-overview-dashboard-strategy-editor": HuiOverviewDashboardStrategyEditor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
import { ReactiveElement } from "lit";
|
|
||||||
import { customElement } from "lit/decorators";
|
|
||||||
import { clamp } from "../../../../common/number/clamp";
|
|
||||||
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
|
|
||||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
|
||||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
|
||||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
|
||||||
import type { HomeAssistant } from "../../../../types";
|
|
||||||
import type { HeadingCardConfig } from "../../cards/types";
|
|
||||||
import {
|
|
||||||
AREA_STRATEGY_GROUP_ICONS,
|
|
||||||
computeAreaTileCardConfig,
|
|
||||||
getAreaGroupedEntities,
|
|
||||||
} from "../areas/helpers/areas-strategy-helper";
|
|
||||||
|
|
||||||
export interface OverviewAreaViewStrategyConfig {
|
|
||||||
type: "overview-area";
|
|
||||||
area?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const computeHeadingCard = (
|
|
||||||
heading: string,
|
|
||||||
icon: string,
|
|
||||||
navigation_path?: string
|
|
||||||
): LovelaceCardConfig =>
|
|
||||||
({
|
|
||||||
type: "heading",
|
|
||||||
heading: heading,
|
|
||||||
icon: icon,
|
|
||||||
tap_action: navigation_path
|
|
||||||
? {
|
|
||||||
action: "navigate",
|
|
||||||
navigation_path,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
}) satisfies HeadingCardConfig;
|
|
||||||
|
|
||||||
@customElement("overview-area-view-strategy")
|
|
||||||
export class OverviewAreaViewStrategy extends ReactiveElement {
|
|
||||||
static async generate(
|
|
||||||
config: OverviewAreaViewStrategyConfig,
|
|
||||||
hass: HomeAssistant
|
|
||||||
): Promise<LovelaceViewConfig> {
|
|
||||||
if (!config.area) {
|
|
||||||
throw new Error("Area not provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
const area = hass.areas[config.area];
|
|
||||||
|
|
||||||
if (!area) {
|
|
||||||
throw new Error("Unknown area");
|
|
||||||
}
|
|
||||||
|
|
||||||
const sections: LovelaceSectionRawConfig[] = [];
|
|
||||||
|
|
||||||
const badges: LovelaceBadgeConfig[] = [];
|
|
||||||
|
|
||||||
if (area.temperature_entity_id) {
|
|
||||||
badges.push({
|
|
||||||
entity: area.temperature_entity_id,
|
|
||||||
type: "entity",
|
|
||||||
color: "red",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (area.humidity_entity_id) {
|
|
||||||
badges.push({
|
|
||||||
entity: area.humidity_entity_id,
|
|
||||||
type: "entity",
|
|
||||||
color: "indigo",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupedEntities = getAreaGroupedEntities(config.area, hass);
|
|
||||||
|
|
||||||
const computeTileCard = computeAreaTileCardConfig(hass, area.name, true);
|
|
||||||
|
|
||||||
const {
|
|
||||||
lights,
|
|
||||||
climate,
|
|
||||||
covers,
|
|
||||||
media_players,
|
|
||||||
security,
|
|
||||||
actions,
|
|
||||||
others,
|
|
||||||
} = groupedEntities;
|
|
||||||
|
|
||||||
if (lights.length > 0) {
|
|
||||||
sections.push({
|
|
||||||
type: "grid",
|
|
||||||
cards: [
|
|
||||||
computeHeadingCard(
|
|
||||||
hass.localize("ui.panel.lovelace.strategy.areas.groups.lights"),
|
|
||||||
AREA_STRATEGY_GROUP_ICONS.lights,
|
|
||||||
"lights"
|
|
||||||
),
|
|
||||||
...lights.map(computeTileCard),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (covers.length > 0) {
|
|
||||||
sections.push({
|
|
||||||
type: "grid",
|
|
||||||
cards: [
|
|
||||||
computeHeadingCard(
|
|
||||||
hass.localize("ui.panel.lovelace.strategy.areas.groups.covers"),
|
|
||||||
AREA_STRATEGY_GROUP_ICONS.covers,
|
|
||||||
"covers"
|
|
||||||
),
|
|
||||||
...covers.map(computeTileCard),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (climate.length > 0) {
|
|
||||||
sections.push({
|
|
||||||
type: "grid",
|
|
||||||
cards: [
|
|
||||||
computeHeadingCard(
|
|
||||||
hass.localize("ui.panel.lovelace.strategy.areas.groups.climate"),
|
|
||||||
AREA_STRATEGY_GROUP_ICONS.climate
|
|
||||||
),
|
|
||||||
...climate.map(computeTileCard),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (media_players.length > 0) {
|
|
||||||
sections.push({
|
|
||||||
type: "grid",
|
|
||||||
cards: [
|
|
||||||
computeHeadingCard(
|
|
||||||
hass.localize(
|
|
||||||
"ui.panel.lovelace.strategy.areas.groups.media_players"
|
|
||||||
),
|
|
||||||
AREA_STRATEGY_GROUP_ICONS.media_players
|
|
||||||
),
|
|
||||||
...media_players.map(computeTileCard),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (security.length > 0) {
|
|
||||||
sections.push({
|
|
||||||
type: "grid",
|
|
||||||
cards: [
|
|
||||||
computeHeadingCard(
|
|
||||||
hass.localize("ui.panel.lovelace.strategy.areas.groups.security"),
|
|
||||||
AREA_STRATEGY_GROUP_ICONS.security
|
|
||||||
),
|
|
||||||
...security.map(computeTileCard),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actions.length > 0) {
|
|
||||||
sections.push({
|
|
||||||
type: "grid",
|
|
||||||
cards: [
|
|
||||||
computeHeadingCard(
|
|
||||||
hass.localize("ui.panel.lovelace.strategy.areas.groups.actions"),
|
|
||||||
AREA_STRATEGY_GROUP_ICONS.actions
|
|
||||||
),
|
|
||||||
...actions.map(computeTileCard),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (others.length > 0) {
|
|
||||||
sections.push({
|
|
||||||
type: "grid",
|
|
||||||
cards: [
|
|
||||||
computeHeadingCard(
|
|
||||||
hass.localize("ui.panel.lovelace.strategy.areas.groups.others"),
|
|
||||||
AREA_STRATEGY_GROUP_ICONS.others
|
|
||||||
),
|
|
||||||
...others.map(computeTileCard),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow between 2 and 3 columns (the max should be set to define the width of the header)
|
|
||||||
const maxColumns = clamp(sections.length, 2, 3);
|
|
||||||
|
|
||||||
// Take the full width if there is only one section to avoid narrow header on desktop
|
|
||||||
if (sections.length === 1) {
|
|
||||||
sections[0].column_span = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "sections",
|
|
||||||
header: {
|
|
||||||
badges_position: "bottom",
|
|
||||||
},
|
|
||||||
max_columns: maxColumns,
|
|
||||||
sections: sections,
|
|
||||||
badges: badges,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"overview-area-view-strategy": OverviewAreaViewStrategy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
import { ReactiveElement } from "lit";
|
|
||||||
import { customElement } from "lit/decorators";
|
|
||||||
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
|
|
||||||
import { clamp } from "../../../../common/number/clamp";
|
|
||||||
import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
|
|
||||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
|
||||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
|
||||||
import type { HomeAssistant } from "../../../../types";
|
|
||||||
import {
|
|
||||||
computeAreaTileCardConfig,
|
|
||||||
getAreas,
|
|
||||||
getFloors,
|
|
||||||
} from "../areas/helpers/areas-strategy-helper";
|
|
||||||
|
|
||||||
export interface OverviewCoversViewStrategyConfig {
|
|
||||||
type: "overview-covers";
|
|
||||||
}
|
|
||||||
|
|
||||||
const UNASSIGNED_FLOOR = "__unassigned__";
|
|
||||||
|
|
||||||
@customElement("overview-covers-view-strategy")
|
|
||||||
export class OverviewCoversViewStrategy extends ReactiveElement {
|
|
||||||
static async generate(
|
|
||||||
_config: OverviewCoversViewStrategyConfig,
|
|
||||||
hass: HomeAssistant
|
|
||||||
): Promise<LovelaceViewConfig> {
|
|
||||||
const areas = getAreas(hass.areas);
|
|
||||||
|
|
||||||
const floors = getFloors(hass.floors);
|
|
||||||
|
|
||||||
const sections: LovelaceSectionRawConfig[] = [];
|
|
||||||
|
|
||||||
const allEntities = Object.keys(hass.states);
|
|
||||||
|
|
||||||
const coverFilter = generateEntityFilter(hass, {
|
|
||||||
domain: "cover",
|
|
||||||
entity_category: "none",
|
|
||||||
});
|
|
||||||
|
|
||||||
const binarySensorFilter = generateEntityFilter(hass, {
|
|
||||||
domain: "binary_sensor",
|
|
||||||
device_class: ["door", "garage_door", "window"],
|
|
||||||
entity_category: "none",
|
|
||||||
});
|
|
||||||
|
|
||||||
const coverEntities = allEntities.filter(coverFilter);
|
|
||||||
const binarySensorEntities = allEntities.filter(binarySensorFilter);
|
|
||||||
|
|
||||||
const entities = [...coverEntities, ...binarySensorEntities];
|
|
||||||
|
|
||||||
const allFloors = [
|
|
||||||
...floors,
|
|
||||||
{
|
|
||||||
floor_id: UNASSIGNED_FLOOR,
|
|
||||||
name: hass.localize("ui.panel.lovelace.strategy.areas.other_areas"),
|
|
||||||
level: null,
|
|
||||||
icon: null,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const floor of allFloors) {
|
|
||||||
let hasCover = false;
|
|
||||||
|
|
||||||
const areasInFloor = areas.filter(
|
|
||||||
(area) =>
|
|
||||||
area.floor_id === floor.floor_id ||
|
|
||||||
(!area.floor_id && floor.floor_id === UNASSIGNED_FLOOR)
|
|
||||||
);
|
|
||||||
|
|
||||||
const noFloors =
|
|
||||||
floors.length === 0 && floor.floor_id === UNASSIGNED_FLOOR;
|
|
||||||
|
|
||||||
const headingTitle = noFloors
|
|
||||||
? hass.localize("ui.panel.lovelace.strategy.areas.areas")
|
|
||||||
: floor.name;
|
|
||||||
|
|
||||||
const section: LovelaceSectionRawConfig = {
|
|
||||||
type: "grid",
|
|
||||||
cards: [
|
|
||||||
{
|
|
||||||
type: "heading",
|
|
||||||
heading: headingTitle,
|
|
||||||
icon: floor.icon || floorDefaultIcon(floor) || "mdi:home-floor",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const area of areasInFloor) {
|
|
||||||
const areaFilter = generateEntityFilter(hass, {
|
|
||||||
area: area.area_id,
|
|
||||||
});
|
|
||||||
const areaEntities = entities.filter(areaFilter);
|
|
||||||
|
|
||||||
if (areaEntities.length > 0) {
|
|
||||||
hasCover = true;
|
|
||||||
section.cards!.push({
|
|
||||||
heading_style: "subtitle",
|
|
||||||
type: "heading",
|
|
||||||
heading: area.name,
|
|
||||||
icon: area.icon || "mdi:home",
|
|
||||||
tap_action: {
|
|
||||||
action: "navigate",
|
|
||||||
navigation_path: `areas-${area.area_id}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const computeTileCard = computeAreaTileCardConfig(
|
|
||||||
hass,
|
|
||||||
area.name,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const entityId of areaEntities) {
|
|
||||||
section.cards!.push(computeTileCard(entityId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasCover) {
|
|
||||||
sections.push(section);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow between 2 and 3 columns (the max should be set to define the width of the header)
|
|
||||||
const maxColumns = clamp(sections.length, 2, 3);
|
|
||||||
|
|
||||||
// Take the full width if there is only one section to avoid narrow header on desktop
|
|
||||||
if (sections.length === 1) {
|
|
||||||
sections[0].column_span = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "sections",
|
|
||||||
max_columns: maxColumns,
|
|
||||||
sections: sections || [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"overview-covers-view-strategy": OverviewCoversViewStrategy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import { STATE_NOT_RUNNING } from "home-assistant-js-websocket";
|
|
||||||
import { ReactiveElement } from "lit";
|
|
||||||
import { customElement } from "lit/decorators";
|
|
||||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
|
||||||
import type { LovelaceViewRawConfig } from "../../../../data/lovelace/config/view";
|
|
||||||
import type { HomeAssistant } from "../../../../types";
|
|
||||||
import {
|
|
||||||
AREA_STRATEGY_GROUP_ICONS,
|
|
||||||
getAreas,
|
|
||||||
} from "../areas/helpers/areas-strategy-helper";
|
|
||||||
import type { LovelaceStrategyEditor } from "../types";
|
|
||||||
import type { OverviewAreaViewStrategyConfig } from "./overview-area-view-strategy";
|
|
||||||
import type { OverviewHomeViewStrategyConfig } from "./overview-home-view-strategy";
|
|
||||||
|
|
||||||
export interface OverviewDashboardStrategyConfig {
|
|
||||||
type: "overview";
|
|
||||||
favorite_entities?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("overview-dashboard-strategy")
|
|
||||||
export class OverviewDashboardStrategy extends ReactiveElement {
|
|
||||||
static async generate(
|
|
||||||
config: OverviewDashboardStrategyConfig,
|
|
||||||
hass: HomeAssistant
|
|
||||||
): Promise<LovelaceConfig> {
|
|
||||||
if (hass.config.state === STATE_NOT_RUNNING) {
|
|
||||||
return {
|
|
||||||
views: [
|
|
||||||
{
|
|
||||||
type: "sections",
|
|
||||||
sections: [{ cards: [{ type: "starting" }] }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hass.config.recovery_mode) {
|
|
||||||
return {
|
|
||||||
views: [
|
|
||||||
{
|
|
||||||
type: "sections",
|
|
||||||
sections: [{ cards: [{ type: "recovery-mode" }] }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const areas = getAreas(hass.areas);
|
|
||||||
|
|
||||||
const areaViews = areas.map<LovelaceViewRawConfig>((area) => {
|
|
||||||
const path = `areas-${area.area_id}`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: area.name,
|
|
||||||
path: path,
|
|
||||||
subview: true,
|
|
||||||
strategy: {
|
|
||||||
type: "overview-area",
|
|
||||||
area: area.area_id,
|
|
||||||
} satisfies OverviewAreaViewStrategyConfig,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const lightView = {
|
|
||||||
title: "Lights",
|
|
||||||
path: "lights",
|
|
||||||
subview: true,
|
|
||||||
strategy: {
|
|
||||||
type: "overview-lights",
|
|
||||||
},
|
|
||||||
icon: AREA_STRATEGY_GROUP_ICONS.lights,
|
|
||||||
} satisfies LovelaceViewRawConfig;
|
|
||||||
|
|
||||||
const coversView = {
|
|
||||||
title: "Covers",
|
|
||||||
path: "covers",
|
|
||||||
subview: true,
|
|
||||||
strategy: {
|
|
||||||
type: "overview-covers",
|
|
||||||
},
|
|
||||||
icon: AREA_STRATEGY_GROUP_ICONS.covers,
|
|
||||||
} satisfies LovelaceViewRawConfig;
|
|
||||||
|
|
||||||
return {
|
|
||||||
views: [
|
|
||||||
{
|
|
||||||
icon: "mdi:home",
|
|
||||||
path: "home",
|
|
||||||
strategy: {
|
|
||||||
type: "overview-home",
|
|
||||||
favorite_entities: config.favorite_entities,
|
|
||||||
} satisfies OverviewHomeViewStrategyConfig,
|
|
||||||
},
|
|
||||||
...areaViews,
|
|
||||||
lightView,
|
|
||||||
coversView,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async getConfigElement(): Promise<LovelaceStrategyEditor> {
|
|
||||||
await import("./editor/hui-overview-dashboard-strategy-editor");
|
|
||||||
return document.createElement("hui-overview-dashboard-strategy-editor");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"overview-dashboard-strategy": OverviewDashboardStrategy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
import { ReactiveElement } from "lit";
|
|
||||||
import { customElement } from "lit/decorators";
|
|
||||||
import { clamp } from "../../../../common/number/clamp";
|
|
||||||
import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
|
|
||||||
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
|
||||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
|
||||||
import type { HomeAssistant } from "../../../../types";
|
|
||||||
import { getAreaControlEntities } from "../../card-features/hui-area-controls-card-feature";
|
|
||||||
import { AREA_CONTROLS, type AreaControl } from "../../card-features/types";
|
|
||||||
import type {
|
|
||||||
AreaCardConfig,
|
|
||||||
ButtonCardConfig,
|
|
||||||
HeadingCardConfig,
|
|
||||||
MarkdownCardConfig,
|
|
||||||
TileCardConfig,
|
|
||||||
} from "../../cards/types";
|
|
||||||
import {
|
|
||||||
AREA_STRATEGY_GROUP_ICONS,
|
|
||||||
getAreas,
|
|
||||||
getFloors,
|
|
||||||
} from "../areas/helpers/areas-strategy-helper";
|
|
||||||
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
|
|
||||||
|
|
||||||
const UNASSIGNED_FLOOR = "__unassigned__";
|
|
||||||
|
|
||||||
export interface OverviewHomeViewStrategyConfig {
|
|
||||||
type: "overview-home";
|
|
||||||
favorite_entities?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("overview-home-view-strategy")
|
|
||||||
export class OverviewHomeViewStrategy extends ReactiveElement {
|
|
||||||
static async generate(
|
|
||||||
config: OverviewHomeViewStrategyConfig,
|
|
||||||
hass: HomeAssistant
|
|
||||||
): Promise<LovelaceViewConfig> {
|
|
||||||
const displayedAreas = getAreas(hass.areas);
|
|
||||||
|
|
||||||
const floors = getFloors(hass.floors);
|
|
||||||
|
|
||||||
const floorSections = [
|
|
||||||
...floors,
|
|
||||||
{
|
|
||||||
floor_id: UNASSIGNED_FLOOR,
|
|
||||||
name: hass.localize("ui.panel.lovelace.strategy.areas.other_areas"),
|
|
||||||
level: null,
|
|
||||||
icon: null,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
.map((floor) => {
|
|
||||||
const areasInFloors = displayedAreas.filter(
|
|
||||||
(area) =>
|
|
||||||
area.floor_id === floor.floor_id ||
|
|
||||||
(!area.floor_id && floor.floor_id === UNASSIGNED_FLOOR)
|
|
||||||
);
|
|
||||||
|
|
||||||
return [floor, areasInFloors] as const;
|
|
||||||
})
|
|
||||||
.filter(([_, areas]) => areas.length)
|
|
||||||
.map<LovelaceSectionConfig | undefined>(([floor, areas], _, array) => {
|
|
||||||
const areasCards = areas.map<AreaCardConfig>((area) => {
|
|
||||||
const path = `areas-${area.area_id}`;
|
|
||||||
|
|
||||||
const controls: AreaControl[] = AREA_CONTROLS.filter(
|
|
||||||
(a) => a !== "switch" // Exclude switches control for areas as we don't know what the switches control
|
|
||||||
);
|
|
||||||
const controlEntities = getAreaControlEntities(
|
|
||||||
controls,
|
|
||||||
area.area_id,
|
|
||||||
[],
|
|
||||||
hass
|
|
||||||
);
|
|
||||||
|
|
||||||
const filteredControls = controls.filter(
|
|
||||||
(control) => controlEntities[control].length > 0
|
|
||||||
);
|
|
||||||
|
|
||||||
const sensorClasses: string[] = [];
|
|
||||||
if (area.temperature_entity_id) {
|
|
||||||
sensorClasses.push("temperature");
|
|
||||||
}
|
|
||||||
if (area.humidity_entity_id) {
|
|
||||||
sensorClasses.push("humidity");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "area",
|
|
||||||
area: area.area_id,
|
|
||||||
display_type: "compact",
|
|
||||||
sensor_classes: sensorClasses,
|
|
||||||
features: filteredControls.length
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
type: "area-controls",
|
|
||||||
controls: filteredControls,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
grid_options: {
|
|
||||||
rows: 1,
|
|
||||||
columns: 12,
|
|
||||||
},
|
|
||||||
features_position: "inline",
|
|
||||||
navigation_path: path,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const noFloors =
|
|
||||||
array.length === 1 && floor.floor_id === UNASSIGNED_FLOOR;
|
|
||||||
|
|
||||||
const headingTitle = noFloors
|
|
||||||
? hass.localize("ui.panel.lovelace.strategy.areas.areas")
|
|
||||||
: floor.name;
|
|
||||||
|
|
||||||
const headingCard: HeadingCardConfig = {
|
|
||||||
type: "heading",
|
|
||||||
heading_style: "title",
|
|
||||||
heading: headingTitle,
|
|
||||||
icon: floor.icon || floorDefaultIcon(floor),
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
max_columns: 3,
|
|
||||||
type: "grid",
|
|
||||||
cards: [headingCard, ...areasCards],
|
|
||||||
};
|
|
||||||
})
|
|
||||||
?.filter((section) => section !== undefined);
|
|
||||||
|
|
||||||
// Allow between 2 and 3 columns (the max should be set to define the width of the header)
|
|
||||||
const maxColumns = clamp(floorSections.length, 2, 3);
|
|
||||||
|
|
||||||
const favoriteSection: LovelaceSectionConfig = {
|
|
||||||
type: "grid",
|
|
||||||
column_span: maxColumns,
|
|
||||||
cards: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const favoriteEntities = (config.favorite_entities || []).filter(
|
|
||||||
(entityId) => hass.states[entityId] !== undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
if (favoriteEntities.length > 0) {
|
|
||||||
favoriteSection.cards!.push(
|
|
||||||
{
|
|
||||||
type: "heading",
|
|
||||||
heading: "Favorites",
|
|
||||||
},
|
|
||||||
...favoriteEntities.map(
|
|
||||||
(entityId) =>
|
|
||||||
({
|
|
||||||
type: "tile",
|
|
||||||
entity: entityId,
|
|
||||||
}) as TileCardConfig
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const personSection: LovelaceSectionConfig = {
|
|
||||||
type: "grid",
|
|
||||||
column_span: maxColumns,
|
|
||||||
cards: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const personFilter = generateEntityFilter(hass, {
|
|
||||||
domain: "person",
|
|
||||||
});
|
|
||||||
|
|
||||||
const personEntities = Object.keys(hass.states).filter(personFilter);
|
|
||||||
|
|
||||||
if (personEntities.length > 0) {
|
|
||||||
personSection.cards!.push(
|
|
||||||
{
|
|
||||||
type: "heading",
|
|
||||||
heading: "People",
|
|
||||||
},
|
|
||||||
...personEntities.map(
|
|
||||||
(entityId) =>
|
|
||||||
({
|
|
||||||
type: "tile",
|
|
||||||
entity: entityId,
|
|
||||||
show_entity_picture: true,
|
|
||||||
}) as TileCardConfig
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const categorySection: LovelaceSectionConfig = {
|
|
||||||
type: "grid",
|
|
||||||
column_span: maxColumns,
|
|
||||||
cards: [
|
|
||||||
{
|
|
||||||
type: "heading",
|
|
||||||
heading: "Categories",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "button",
|
|
||||||
icon: AREA_STRATEGY_GROUP_ICONS.lights,
|
|
||||||
name: "Lights",
|
|
||||||
grid_options: {
|
|
||||||
rows: 2,
|
|
||||||
columns: 4,
|
|
||||||
},
|
|
||||||
tap_action: {
|
|
||||||
action: "navigate",
|
|
||||||
navigation_path: "lights",
|
|
||||||
},
|
|
||||||
} satisfies ButtonCardConfig,
|
|
||||||
{
|
|
||||||
type: "button",
|
|
||||||
icon: AREA_STRATEGY_GROUP_ICONS.covers,
|
|
||||||
name: "Covers",
|
|
||||||
grid_options: {
|
|
||||||
rows: 2,
|
|
||||||
columns: 4,
|
|
||||||
},
|
|
||||||
tap_action: {
|
|
||||||
action: "navigate",
|
|
||||||
navigation_path: "covers",
|
|
||||||
},
|
|
||||||
} satisfies ButtonCardConfig,
|
|
||||||
{
|
|
||||||
type: "button",
|
|
||||||
icon: "mdi:lightning-bolt",
|
|
||||||
name: "Energy",
|
|
||||||
grid_options: {
|
|
||||||
rows: 2,
|
|
||||||
columns: 4,
|
|
||||||
},
|
|
||||||
tap_action: {
|
|
||||||
action: "navigate",
|
|
||||||
navigation_path: "/energy?historyBack=1",
|
|
||||||
},
|
|
||||||
} satisfies ButtonCardConfig,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const sections = [
|
|
||||||
...(favoriteSection.cards ? [favoriteSection] : []),
|
|
||||||
...(personSection.cards ? [personSection] : []),
|
|
||||||
categorySection,
|
|
||||||
...floorSections,
|
|
||||||
];
|
|
||||||
return {
|
|
||||||
type: "sections",
|
|
||||||
max_columns: maxColumns,
|
|
||||||
sections: sections,
|
|
||||||
header: {
|
|
||||||
layout: "responsive",
|
|
||||||
card: {
|
|
||||||
type: "markdown",
|
|
||||||
text_only: true,
|
|
||||||
content: "## Welcome {{user}} !",
|
|
||||||
} satisfies MarkdownCardConfig,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"overview-home-view-strategy": OverviewHomeViewStrategy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
import { ReactiveElement } from "lit";
|
|
||||||
import { customElement } from "lit/decorators";
|
|
||||||
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
|
|
||||||
import { clamp } from "../../../../common/number/clamp";
|
|
||||||
import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
|
|
||||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
|
||||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
|
||||||
import type { HomeAssistant } from "../../../../types";
|
|
||||||
import {
|
|
||||||
computeAreaTileCardConfig,
|
|
||||||
getAreas,
|
|
||||||
getFloors,
|
|
||||||
} from "../areas/helpers/areas-strategy-helper";
|
|
||||||
|
|
||||||
export interface OverviewLightsViewStrategyConfig {
|
|
||||||
type: "overview-lights";
|
|
||||||
}
|
|
||||||
|
|
||||||
const UNASSIGNED_FLOOR = "__unassigned__";
|
|
||||||
|
|
||||||
@customElement("overview-lights-view-strategy")
|
|
||||||
export class OverviewLightsViewStrategy extends ReactiveElement {
|
|
||||||
static async generate(
|
|
||||||
_config: OverviewLightsViewStrategyConfig,
|
|
||||||
hass: HomeAssistant
|
|
||||||
): Promise<LovelaceViewConfig> {
|
|
||||||
const areas = getAreas(hass.areas);
|
|
||||||
|
|
||||||
const floors = getFloors(hass.floors);
|
|
||||||
|
|
||||||
const sections: LovelaceSectionRawConfig[] = [];
|
|
||||||
|
|
||||||
const allEntities = Object.keys(hass.states);
|
|
||||||
|
|
||||||
const lightFilter = generateEntityFilter(hass, {
|
|
||||||
domain: "light",
|
|
||||||
entity_category: "none",
|
|
||||||
});
|
|
||||||
|
|
||||||
const lightsEntities = allEntities.filter(lightFilter);
|
|
||||||
|
|
||||||
const allFloors = [
|
|
||||||
...floors,
|
|
||||||
{
|
|
||||||
floor_id: UNASSIGNED_FLOOR,
|
|
||||||
name: hass.localize("ui.panel.lovelace.strategy.areas.other_areas"),
|
|
||||||
level: null,
|
|
||||||
icon: null,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const floor of allFloors) {
|
|
||||||
let hasLight = false;
|
|
||||||
|
|
||||||
const areasInFloor = areas.filter(
|
|
||||||
(area) =>
|
|
||||||
area.floor_id === floor.floor_id ||
|
|
||||||
(!area.floor_id && floor.floor_id === UNASSIGNED_FLOOR)
|
|
||||||
);
|
|
||||||
|
|
||||||
const noFloors =
|
|
||||||
floors.length === 0 && floor.floor_id === UNASSIGNED_FLOOR;
|
|
||||||
|
|
||||||
const headingTitle = noFloors
|
|
||||||
? hass.localize("ui.panel.lovelace.strategy.areas.areas")
|
|
||||||
: floor.name;
|
|
||||||
|
|
||||||
const section: LovelaceSectionRawConfig = {
|
|
||||||
type: "grid",
|
|
||||||
cards: [
|
|
||||||
{
|
|
||||||
type: "heading",
|
|
||||||
heading: headingTitle,
|
|
||||||
icon: floor.icon || floorDefaultIcon(floor) || "mdi:home-floor",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const area of areasInFloor) {
|
|
||||||
const areaFilter = generateEntityFilter(hass, {
|
|
||||||
area: area.area_id,
|
|
||||||
});
|
|
||||||
const areaLights = lightsEntities.filter(areaFilter);
|
|
||||||
|
|
||||||
if (areaLights.length > 0) {
|
|
||||||
hasLight = true;
|
|
||||||
section.cards!.push({
|
|
||||||
heading_style: "subtitle",
|
|
||||||
type: "heading",
|
|
||||||
heading: area.name,
|
|
||||||
icon: area.icon || "mdi:home",
|
|
||||||
tap_action: {
|
|
||||||
action: "navigate",
|
|
||||||
navigation_path: `areas-${area.area_id}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const computeTileCard = computeAreaTileCardConfig(
|
|
||||||
hass,
|
|
||||||
area.name,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const entityId of areaLights) {
|
|
||||||
section.cards!.push(computeTileCard(entityId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasLight) {
|
|
||||||
sections.push(section);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow between 2 and 3 columns (the max should be set to define the width of the header)
|
|
||||||
const maxColumns = clamp(sections.length, 2, 3);
|
|
||||||
|
|
||||||
// Take the full width if there is only one section to avoid narrow header on desktop
|
|
||||||
if (sections.length === 1) {
|
|
||||||
sections[0].column_span = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "sections",
|
|
||||||
max_columns: maxColumns,
|
|
||||||
sections: sections || [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"overview-lights-view-strategy": OverviewLightsViewStrategy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1168,12 +1168,6 @@
|
|||||||
},
|
},
|
||||||
"summary": "Summary",
|
"summary": "Summary",
|
||||||
"description": "Description"
|
"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": {
|
"attributes": {
|
||||||
@@ -1251,7 +1245,6 @@
|
|||||||
"person": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::person%]",
|
"person": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::person%]",
|
||||||
"zone": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::zone%]",
|
"zone": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::zone%]",
|
||||||
"input_boolean": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::input_boolean%]",
|
"input_boolean": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::input_boolean%]",
|
||||||
"input_button": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::input_button%]",
|
|
||||||
"input_text": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::input_text%]",
|
"input_text": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::input_text%]",
|
||||||
"input_number": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::input_number%]",
|
"input_number": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::input_number%]",
|
||||||
"input_datetime": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::input_datetime%]",
|
"input_datetime": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::input_datetime%]",
|
||||||
@@ -1566,7 +1559,7 @@
|
|||||||
},
|
},
|
||||||
"unavailable": "This entity is unavailable.",
|
"unavailable": "This entity is unavailable.",
|
||||||
"entity_status": "Entity status",
|
"entity_status": "Entity status",
|
||||||
"change_area": "Change area",
|
"change_area": "Change Area",
|
||||||
"enabled_label": "Enabled",
|
"enabled_label": "Enabled",
|
||||||
"disabled_label": "Disabled",
|
"disabled_label": "Disabled",
|
||||||
"enabled_cause": "Cannot change status. Disabled by {cause}.",
|
"enabled_cause": "Cannot change status. Disabled by {cause}.",
|
||||||
@@ -2122,7 +2115,7 @@
|
|||||||
"my": {
|
"my": {
|
||||||
"not_supported": "This redirect is not supported by your Home Assistant instance. Check the {link} for the supported redirects and the version they where introduced.",
|
"not_supported": "This redirect is not supported by your Home Assistant instance. Check the {link} for the supported redirects and the version they where introduced.",
|
||||||
"component_not_loaded": "This redirect is not supported by your Home Assistant instance. You need the integration {integration} to use this redirect.",
|
"component_not_loaded": "This redirect is not supported by your Home Assistant instance. You need the integration {integration} to use this redirect.",
|
||||||
"no_supervisor": "This redirect is not supported by your Home Assistant installation. It needs either the Home Assistant OS or Home Assistant Supervised installation method. For more information, see the {docs_link}.",
|
"no_supervisor": "This redirect is not supported by your Home Assistant installation. It needs either the Home Assistant Operating System or Home Assistant Supervised installation method. For more information, see the {docs_link}.",
|
||||||
"not_app": "This redirect only works from a mobile device that has the Home Assistant Companion app installed. {link}.",
|
"not_app": "This redirect only works from a mobile device that has the Home Assistant Companion app installed. {link}.",
|
||||||
"url_error": "The provided URL is invalid.",
|
"url_error": "The provided URL is invalid.",
|
||||||
"documentation": "documentation",
|
"documentation": "documentation",
|
||||||
@@ -3351,10 +3344,6 @@
|
|||||||
"default": {
|
"default": {
|
||||||
"title": "Default dashboard",
|
"title": "Default dashboard",
|
||||||
"description": "Display your devices grouped by area"
|
"description": "Display your devices grouped by area"
|
||||||
},
|
|
||||||
"overview": {
|
|
||||||
"title": "Overview (experimental)",
|
|
||||||
"description": "Global overview of your home"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"search_dashboards": "Search dashboards",
|
"search_dashboards": "Search dashboards",
|
||||||
@@ -4054,7 +4043,7 @@
|
|||||||
"description": {
|
"description": {
|
||||||
"picker": "Periodically, at a defined interval.",
|
"picker": "Periodically, at a defined interval.",
|
||||||
"initial": "When a time pattern matches",
|
"initial": "When a time pattern matches",
|
||||||
"invalid": "Invalid time pattern for {parts}",
|
"invalid": "Invalid Time Pattern for {parts}",
|
||||||
"full": "Trigger {secondsChoice, select, \n every {every second of }\n every_interval {every {seconds} seconds of }\n on_the_xth {on the {secondsWithOrdinal} second of }\n other {}\n} {minutesChoice, select, \n every {every minute of }\n every_interval {every {minutes} minutes of }\n has_seconds {the {minutesWithOrdinal} minute of }\n on_the_xth {on the {minutesWithOrdinal} minute of }\n other {}\n} {hoursChoice, select, \n every {every hour}\n every_interval {every {hours} hours}\n has_seconds_or_minutes {the {hoursWithOrdinal} hour}\n on_the_xth {on the {hoursWithOrdinal} hour}\n other {}\n}",
|
"full": "Trigger {secondsChoice, select, \n every {every second of }\n every_interval {every {seconds} seconds of }\n on_the_xth {on the {secondsWithOrdinal} second of }\n other {}\n} {minutesChoice, select, \n every {every minute of }\n every_interval {every {minutes} minutes of }\n has_seconds {the {minutesWithOrdinal} minute of }\n on_the_xth {on the {minutesWithOrdinal} minute of }\n other {}\n} {hoursChoice, select, \n every {every hour}\n every_interval {every {hours} hours}\n has_seconds_or_minutes {the {hoursWithOrdinal} hour}\n on_the_xth {on the {hoursWithOrdinal} hour}\n other {}\n}",
|
||||||
"ordinal": "{part, selectordinal, \none {#st}\ntwo {#nd}\nfew {#rd}\nother {#th}\n}"
|
"ordinal": "{part, selectordinal, \none {#st}\ntwo {#nd}\nfew {#rd}\nother {#th}\n}"
|
||||||
}
|
}
|
||||||
@@ -5302,10 +5291,10 @@
|
|||||||
"new_person": "New person",
|
"new_person": "New person",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"name_error_msg": "Name is required",
|
"name_error_msg": "Name is required",
|
||||||
"linked_user": "Linked user",
|
"linked_user": "Linked User",
|
||||||
"device_tracker_intro": "Select the devices that belong to this person",
|
"device_tracker_intro": "Select the devices that belong to this person",
|
||||||
"no_device_tracker_available_intro": "When you have devices that indicate the presence of a person, you will be able to assign them to a person here. You can add your first device by adding a presence detection integration from the integrations page.",
|
"no_device_tracker_available_intro": "When you have devices that indicate the presence of a person, you will be able to assign them to a person here. You can add your first device by adding a presence-detection integration from the integrations page.",
|
||||||
"link_presence_detection_integrations": "Presence detection integrations",
|
"link_presence_detection_integrations": "Presence Detection Integrations",
|
||||||
"link_integrations_page": "Integrations page",
|
"link_integrations_page": "Integrations page",
|
||||||
"device_tracker_picked": "Track device",
|
"device_tracker_picked": "Track device",
|
||||||
"device_tracker_pick": "Pick device to track",
|
"device_tracker_pick": "Pick device to track",
|
||||||
@@ -5601,7 +5590,7 @@
|
|||||||
"local_access_only_description": "Can only log in from the local network",
|
"local_access_only_description": "Can only log in from the local network",
|
||||||
"system_generated": "System user",
|
"system_generated": "System user",
|
||||||
"system_generated_read_only_users": "System users can not be updated.",
|
"system_generated_read_only_users": "System users can not be updated.",
|
||||||
"unnamed_user": "Unnamed user",
|
"unnamed_user": "Unnamed User",
|
||||||
"confirm_user_deletion_title": "Delete {name}?",
|
"confirm_user_deletion_title": "Delete {name}?",
|
||||||
"confirm_user_deletion_text": "This user will be permanently deleted."
|
"confirm_user_deletion_text": "This user will be permanently deleted."
|
||||||
},
|
},
|
||||||
@@ -6683,7 +6672,7 @@
|
|||||||
"not_supported": {
|
"not_supported": {
|
||||||
"title": "The operating system does not support network storage",
|
"title": "The operating system does not support network storage",
|
||||||
"supervised": "Network storage is not supported on this host",
|
"supervised": "Network storage is not supported on this host",
|
||||||
"os": "To use network storage you need to run at least Home Assistant OS {version}",
|
"os": "To use network storage you need to run at least Home Assistant Operating System {version}",
|
||||||
"navigate_to_updates": "Go to updates"
|
"navigate_to_updates": "Go to updates"
|
||||||
},
|
},
|
||||||
"mount_usage": {
|
"mount_usage": {
|
||||||
@@ -7494,7 +7483,7 @@
|
|||||||
"day": "Day",
|
"day": "Day",
|
||||||
"month": "Month",
|
"month": "Month",
|
||||||
"week": "Week",
|
"week": "Week",
|
||||||
"5minute": "5 minutes"
|
"5minute": "5 Minutes"
|
||||||
},
|
},
|
||||||
"pick_statistic": "Add a statistic",
|
"pick_statistic": "Add a statistic",
|
||||||
"picked_statistic": "Statistic",
|
"picked_statistic": "Statistic",
|
||||||
@@ -7734,11 +7723,6 @@
|
|||||||
"hide_completed": "Hide completed items",
|
"hide_completed": "Hide completed items",
|
||||||
"hide_create": "Hide 'Add item' field",
|
"hide_create": "Hide 'Add item' field",
|
||||||
"display_order": "Display order",
|
"display_order": "Display order",
|
||||||
"item_tap_action": "Item tap behavior",
|
|
||||||
"actions": {
|
|
||||||
"edit": "Default (edit item)",
|
|
||||||
"toggle": "Toggle item"
|
|
||||||
},
|
|
||||||
"sort_modes": {
|
"sort_modes": {
|
||||||
"none": "Default",
|
"none": "Default",
|
||||||
"manual": "Manual",
|
"manual": "Manual",
|
||||||
@@ -8059,8 +8043,8 @@
|
|||||||
},
|
},
|
||||||
"no_compatible_controls": "No compatible controls available for this area"
|
"no_compatible_controls": "No compatible controls available for this area"
|
||||||
},
|
},
|
||||||
"progress-bar": {
|
"history-chart": {
|
||||||
"label": "Progress bar"
|
"label": "History chart"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -8935,7 +8919,7 @@
|
|||||||
"uploading": "[%key:ui::components::file-upload::uploading%]",
|
"uploading": "[%key:ui::components::file-upload::uploading%]",
|
||||||
"details": {
|
"details": {
|
||||||
"home_assistant_missing": "This backup does not include your Home Assistant configuration, you cannot use it to restore your instance.",
|
"home_assistant_missing": "This backup does not include your Home Assistant configuration, you cannot use it to restore your instance.",
|
||||||
"addons_unsupported": "Your installation method doesn’t support add-ons. If you want to restore these, you have to install Home Assistant OS",
|
"addons_unsupported": "Your installation method doesn’t support add-ons. If you want to restore these, you have to install Home Assistant Operating System",
|
||||||
"summary": {
|
"summary": {
|
||||||
"created": "[%key:ui::panel::config::backup::details::summary::created%]",
|
"created": "[%key:ui::panel::config::backup::details::summary::created%]",
|
||||||
"content": "Content"
|
"content": "Content"
|
||||||
@@ -9310,7 +9294,7 @@
|
|||||||
"rebuild": "Failed to rebuild add-on",
|
"rebuild": "Failed to rebuild add-on",
|
||||||
"restart": "Failed to restart add-on",
|
"restart": "Failed to restart add-on",
|
||||||
"start": "Failed to start add-on",
|
"start": "Failed to start add-on",
|
||||||
"go_to_config": "Edit config",
|
"go_to_config": "Edit Config",
|
||||||
"start_invalid_config": "Go to configuration",
|
"start_invalid_config": "Go to configuration",
|
||||||
"validate_config": "Failed to validate add-on configuration",
|
"validate_config": "Failed to validate add-on configuration",
|
||||||
"get_changelog": "Failed to get add-on changelog"
|
"get_changelog": "Failed to get add-on changelog"
|
||||||
|
|||||||
Reference in New Issue
Block a user