Compare commits
41 Commits
improve_ne
...
edit_card_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43374ef798 | ||
|
|
bb672d0272 | ||
|
|
e26d3d39f0 | ||
|
|
e54c3a69af | ||
|
|
cc04457d72 | ||
|
|
9e1d64e728 | ||
|
|
0cfe7f8d12 | ||
|
|
2b1f301db6 | ||
|
|
fc4996412e | ||
|
|
ece4a6345f | ||
|
|
a4c08a78b9 | ||
|
|
a438fc5e41 | ||
|
|
783132ae46 | ||
|
|
680d81001c | ||
|
|
a917383d7a | ||
|
|
455a6761cd | ||
|
|
acf42d7637 | ||
|
|
3857c7321a | ||
|
|
5eec814988 | ||
|
|
edd37565a6 | ||
|
|
fb3f779121 | ||
|
|
4d7634ac67 | ||
|
|
ba5c1133c6 | ||
|
|
0a05dd8f71 | ||
|
|
400106ec09 | ||
|
|
a7a4194e09 | ||
|
|
0bd7d27c57 | ||
|
|
8175e45921 | ||
|
|
cae36b393b | ||
|
|
f84ad92356 | ||
|
|
fb1ee2ed1d | ||
|
|
9073282174 | ||
|
|
91bd5cba08 | ||
|
|
a68bdbfe08 | ||
|
|
f3d614b0d3 | ||
|
|
f3c9e4a4a0 | ||
|
|
d22a82c4a6 | ||
|
|
5cddc6e5c6 | ||
|
|
c5c067ef19 | ||
|
|
694bb3088c | ||
|
|
ad487470fd |
6
.github/workflows/ci.yaml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||
- name: Setup lint cache
|
||||
uses: actions/cache@v4.2.0
|
||||
uses: actions/cache@v4.2.1
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: frontend-bundle-stats
|
||||
path: build/stats/*.json
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: supervisor-bundle-stats
|
||||
path: build/stats/*.json
|
||||
|
||||
4
.github/workflows/nightly.yaml
vendored
@@ -57,14 +57,14 @@ jobs:
|
||||
run: tar -czvf translations.tar.gz translations
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: wheels
|
||||
path: dist/home_assistant_frontend*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
|
||||
@@ -94,10 +94,6 @@ function copyMapPanel(staticDir) {
|
||||
npmPath("leaflet.markercluster/dist/MarkerCluster.css"),
|
||||
staticPath("images/leaflet/")
|
||||
);
|
||||
copyFileDir(
|
||||
npmPath("leaflet.markercluster/dist/MarkerCluster.Default.css"),
|
||||
staticPath("images/leaflet/")
|
||||
);
|
||||
fs.copySync(
|
||||
npmPath("leaflet/dist/images"),
|
||||
staticPath("images/leaflet/images/")
|
||||
|
||||
10
gallery/public/images/select_box/card.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="94" height="64" viewBox="0 0 94 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="64" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="63" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M8 27C8 25.3431 9.34315 24 11 24H31C32.6569 24 34 25.3431 34 27V29C34 30.6569 32.6569 32 31 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M38 27C38 25.3431 39.3431 24 41 24H83C84.6569 24 86 25.3431 86 27V29C86 30.6569 84.6569 32 83 32H41C39.3431 32 38 30.6569 38 29V27Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M8 39C8 37.3431 9.34315 36 11 36H53C54.6569 36 56 37.3431 56 39V41C56 42.6569 54.6569 44 53 44H11C9.34315 44 8 42.6569 8 41V39Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M60 39C60 37.3431 61.3431 36 63 36H83C84.6569 36 86 37.3431 86 39V41C86 42.6569 84.6569 44 83 44H63C61.3431 44 60 42.6569 60 41V39Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M8 51C8 49.3431 9.34315 48 11 48H31C32.6569 48 34 49.3431 34 51V53C34 54.6569 32.6569 56 31 56H11C9.34315 56 8 54.6569 8 53V51Z" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
7
gallery/public/images/select_box/text_only.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="94" height="48" viewBox="0 0 94 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 11C0 9.34315 1.34315 8 3 8H23C24.6569 8 26 9.34315 26 11V13C26 14.6569 24.6569 16 23 16H3C1.34315 16 0 14.6569 0 13V11Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M30 11C30 9.34315 31.3431 8 33 8H91C92.6569 8 94 9.34315 94 11V13C94 14.6569 92.6569 16 91 16H33C31.3431 16 30 14.6569 30 13V11Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M0 23C0 21.3431 1.34315 20 3 20H61C62.6569 20 64 21.3431 64 23V25C64 26.6569 62.6569 28 61 28H3C1.34315 28 0 26.6569 0 25V23Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M68 23C68 21.3431 69.3431 20 71 20H91C92.6569 20 94 21.3431 94 23V25C94 26.6569 92.6569 28 91 28H71C69.3431 28 68 26.6569 68 25V23Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M0 35C0 33.3431 1.34315 32 3 32H23C24.6569 32 26 33.3431 26 35V37C26 38.6569 24.6569 40 23 40H3C1.34315 40 0 38.6569 0 37V35Z" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 964 B |
3
gallery/src/pages/components/ha-select-box.markdown
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Select box
|
||||
---
|
||||
152
gallery/src/pages/components/ha-select-box.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-select-box";
|
||||
import type { SelectBoxOption } from "../../../../src/components/ha-select-box";
|
||||
|
||||
const basicOptions: SelectBoxOption[] = [
|
||||
{
|
||||
value: "text-only",
|
||||
label: "Text only",
|
||||
},
|
||||
{
|
||||
value: "card",
|
||||
label: "Card",
|
||||
},
|
||||
{
|
||||
value: "disabled",
|
||||
label: "Disabled option",
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
const fullOptions: SelectBoxOption[] = [
|
||||
{
|
||||
value: "text-only",
|
||||
label: "Text only",
|
||||
description: "Only text, no border and background",
|
||||
image: "/images/select_box/text_only.svg",
|
||||
},
|
||||
{
|
||||
value: "card",
|
||||
label: "Card",
|
||||
description: "With border and background",
|
||||
image: "/images/select_box/card.svg",
|
||||
},
|
||||
{
|
||||
value: "disabled",
|
||||
label: "Disabled",
|
||||
description: "Option that can not be selected",
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
const selects: {
|
||||
id: string;
|
||||
label: string;
|
||||
class?: string;
|
||||
options: SelectBoxOption[];
|
||||
disabled?: boolean;
|
||||
}[] = [
|
||||
{
|
||||
id: "basic",
|
||||
label: "Basic",
|
||||
options: basicOptions,
|
||||
},
|
||||
{
|
||||
id: "full",
|
||||
label: "With description and image",
|
||||
options: fullOptions,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-components-ha-select-box")
|
||||
export class DemoHaSelectBox extends LitElement {
|
||||
@state() private value?: string = "off";
|
||||
|
||||
handleValueChanged(e: CustomEvent) {
|
||||
this.value = e.detail.value as string;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${repeat(selects, (select) => {
|
||||
const { id, label, options } = select;
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<label id=${id}>${label}</label>
|
||||
<ha-select-box
|
||||
.value=${this.value}
|
||||
.options=${options}
|
||||
@value-changed=${this.handleValueChanged}
|
||||
>
|
||||
</ha-select-box>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
})}
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<p class="title"><b>Column layout</b></p>
|
||||
<div class="vertical-selects">
|
||||
${repeat(selects, (select) => {
|
||||
const { options } = select;
|
||||
return html`
|
||||
<ha-select-box
|
||||
.value=${this.value}
|
||||
.options=${options}
|
||||
.maxColumns=${1}
|
||||
@value-changed=${this.handleValueChanged}
|
||||
>
|
||||
</ha-select-box>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
label {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
.custom {
|
||||
--mdc-icon-size: 24px;
|
||||
--control-select-color: var(--state-fan-active-color);
|
||||
--control-select-thickness: 130px;
|
||||
--control-select-border-radius: 36px;
|
||||
}
|
||||
|
||||
p.title {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.vertical-selects ha-select-box {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-select-box": DemoHaSelectBox;
|
||||
}
|
||||
}
|
||||
30
gallery/src/pages/components/ha-tooltip.markdown
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: Tooltip
|
||||
---
|
||||
|
||||
A tooltip's target is its _first child element_, so you should only wrap one element inside of the tooltip. If you need the tooltip to show up for multiple elements, nest them inside a container first.
|
||||
|
||||
Tooltips use `display: contents` so they won't interfere with how elements are positioned in a flex or grid layout.
|
||||
|
||||
<ha-tooltip content="This is a tooltip">
|
||||
<ha-button>Hover Me</ha-button>
|
||||
</ha-tooltip>
|
||||
|
||||
```
|
||||
<ha-tooltip content="This is a tooltip">
|
||||
<ha-button>Hover Me</ha-button>
|
||||
</ha-tooltip>
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
This element is based on sholace `sl-tooltip` it only sets some css tokens and has a custom show/hide animation.
|
||||
|
||||
<a href="https://shoelace.style/components/tooltip" target="_blank" rel="noopener noreferrer">Shoelace documentation</a>
|
||||
|
||||
### HA style tokens
|
||||
|
||||
In your theme settings use this without the prefixed `--`.
|
||||
|
||||
- `--ha-tooltip-border-radius` (Default: 4px)
|
||||
- `--ha-tooltip-arrow-size` (Default: 8px)
|
||||
2
gallery/src/pages/components/ha-tooltip.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import "../../../../src/components/ha-tooltip";
|
||||
import "../../../../src/components/ha-button";
|
||||
@@ -17,6 +17,7 @@ import "../../../src/components/ha-alert";
|
||||
import {
|
||||
ALTERNATIVE_DNS_SERVERS,
|
||||
getSupervisorNetworkInfo,
|
||||
pingSupervisor,
|
||||
setSupervisorNetworkDns,
|
||||
} from "../data/supervisor";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
@@ -85,7 +86,28 @@ class LandingPageNetwork extends LitElement {
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
this._fetchSupervisorInfo();
|
||||
this._pingSupervisor();
|
||||
}
|
||||
|
||||
private _schedulePingSupervisor() {
|
||||
setTimeout(
|
||||
() => this._pingSupervisor(),
|
||||
SCHEDULE_FETCH_NETWORK_INFO_SECONDS * 1000
|
||||
);
|
||||
}
|
||||
|
||||
private async _pingSupervisor() {
|
||||
try {
|
||||
const response = await pingSupervisor();
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to ping supervisor, assume update in progress");
|
||||
}
|
||||
this._fetchSupervisorInfo();
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
this._schedulePingSupervisor();
|
||||
}
|
||||
}
|
||||
|
||||
private _scheduleFetchSupervisorInfo() {
|
||||
|
||||
@@ -18,7 +18,7 @@ export const ALTERNATIVE_DNS_SERVERS: {
|
||||
];
|
||||
|
||||
export async function getSupervisorLogs(lines = 100) {
|
||||
return fetch(`/supervisor/supervisor/logs?lines=${lines}`, {
|
||||
return fetch(`/supervisor-api/supervisor/logs?lines=${lines}`, {
|
||||
headers: {
|
||||
Accept: "text/plain",
|
||||
},
|
||||
@@ -26,22 +26,26 @@ export async function getSupervisorLogs(lines = 100) {
|
||||
}
|
||||
|
||||
export async function getSupervisorLogsFollow(lines = 500) {
|
||||
return fetch(`/supervisor/supervisor/logs/follow?lines=${lines}`, {
|
||||
return fetch(`/supervisor-api/supervisor/logs/follow?lines=${lines}`, {
|
||||
headers: {
|
||||
Accept: "text/plain",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function pingSupervisor() {
|
||||
return fetch("/supervisor-api/supervisor/ping");
|
||||
}
|
||||
|
||||
export async function getSupervisorNetworkInfo() {
|
||||
return fetch("/supervisor/network/info");
|
||||
return fetch("/supervisor-api/network/info");
|
||||
}
|
||||
|
||||
export const setSupervisorNetworkDns = async (
|
||||
dnsServerIndex: number,
|
||||
primaryInterface: string
|
||||
) =>
|
||||
fetch(`/supervisor/network/interface/${primaryInterface}/update`, {
|
||||
fetch(`/supervisor-api/network/interface/${primaryInterface}/update`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
ipv4: {
|
||||
|
||||
22
package.json
@@ -34,7 +34,7 @@
|
||||
"@codemirror/legacy-modes": "6.4.3",
|
||||
"@codemirror/search": "6.5.9",
|
||||
"@codemirror/state": "6.5.2",
|
||||
"@codemirror/view": "6.36.2",
|
||||
"@codemirror/view": "6.36.3",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.17.3",
|
||||
"@formatjs/intl-displaynames": "6.8.10",
|
||||
@@ -90,6 +90,7 @@
|
||||
"@polymer/paper-tabs": "3.1.0",
|
||||
"@polymer/polymer": "3.5.2",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@shoelace-style/shoelace": "2.20.0",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.6.5",
|
||||
"@vaadin/vaadin-themable-mixin": "24.6.5",
|
||||
@@ -161,13 +162,13 @@
|
||||
"@babel/preset-env": "7.26.9",
|
||||
"@babel/preset-typescript": "7.26.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.18.2",
|
||||
"@lokalise/node-api": "13.1.0",
|
||||
"@lokalise/node-api": "13.2.1",
|
||||
"@octokit/auth-oauth-device": "7.1.3",
|
||||
"@octokit/plugin-retry": "7.1.4",
|
||||
"@octokit/rest": "21.1.1",
|
||||
"@rsdoctor/rspack-plugin": "0.4.13",
|
||||
"@rspack/cli": "1.2.3",
|
||||
"@rspack/core": "1.2.3",
|
||||
"@rspack/cli": "1.2.5",
|
||||
"@rspack/core": "1.2.5",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.21",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
@@ -186,12 +187,12 @@
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "3.0.5",
|
||||
"@vitest/coverage-v8": "3.0.6",
|
||||
"babel-loader": "9.2.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.0",
|
||||
"eslint": "9.20.1",
|
||||
"eslint": "9.21.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.0.1",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
@@ -199,7 +200,7 @@
|
||||
"eslint-plugin-lit": "1.15.0",
|
||||
"eslint-plugin-lit-a11y": "4.1.4",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"eslint-plugin-wc": "2.2.0",
|
||||
"eslint-plugin-wc": "2.2.1",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.0",
|
||||
"glob": "11.0.1",
|
||||
@@ -217,7 +218,7 @@
|
||||
"lodash.template": "4.5.0",
|
||||
"map-stream": "0.0.7",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.5.1",
|
||||
"prettier": "3.5.2",
|
||||
"rspack-manifest-plugin": "5.0.3",
|
||||
"serve": "14.2.4",
|
||||
"sinon": "19.0.2",
|
||||
@@ -226,7 +227,8 @@
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.7.3",
|
||||
"typescript-eslint": "8.24.1",
|
||||
"vitest": "3.0.5",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.0.6",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||
@@ -240,7 +242,7 @@
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "1.6.3",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"globals": "15.15.0",
|
||||
"globals": "16.0.0",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"packageManager": "yarn@4.6.0"
|
||||
|
||||
10
public/static/images/form/markdown_card.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="94" height="64" viewBox="0 0 94 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="64" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="63" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M8 27C8 25.3431 9.34315 24 11 24H31C32.6569 24 34 25.3431 34 27V29C34 30.6569 32.6569 32 31 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M38 27C38 25.3431 39.3431 24 41 24H83C84.6569 24 86 25.3431 86 27V29C86 30.6569 84.6569 32 83 32H41C39.3431 32 38 30.6569 38 29V27Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M8 39C8 37.3431 9.34315 36 11 36H53C54.6569 36 56 37.3431 56 39V41C56 42.6569 54.6569 44 53 44H11C9.34315 44 8 42.6569 8 41V39Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M60 39C60 37.3431 61.3431 36 63 36H83C84.6569 36 86 37.3431 86 39V41C86 42.6569 84.6569 44 83 44H63C61.3431 44 60 42.6569 60 41V39Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M8 51C8 49.3431 9.34315 48 11 48H31C32.6569 48 34 49.3431 34 51V53C34 54.6569 32.6569 56 31 56H11C9.34315 56 8 54.6569 8 53V51Z" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
10
public/static/images/form/markdown_card_dark.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="94" height="64" viewBox="0 0 94 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H86C90.4183 0 94 3.58172 94 8V56C94 60.4183 90.4183 64 86 64H8C3.58172 64 0 60.4183 0 56V8Z" fill="black"/>
|
||||
<path d="M0.5 8C0.5 3.85786 3.85786 0.5 8 0.5H86C90.1421 0.5 93.5 3.85786 93.5 8V56C93.5 60.1421 90.1421 63.5 86 63.5H8C3.85786 63.5 0.5 60.1421 0.5 56V8Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M8 27C8 25.3431 9.34315 24 11 24H31C32.6569 24 34 25.3431 34 27V29C34 30.6569 32.6569 32 31 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M38 27C38 25.3431 39.3431 24 41 24H83C84.6569 24 86 25.3431 86 27V29C86 30.6569 84.6569 32 83 32H41C39.3431 32 38 30.6569 38 29V27Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M8 39C8 37.3431 9.34315 36 11 36H53C54.6569 36 56 37.3431 56 39V41C56 42.6569 54.6569 44 53 44H11C9.34315 44 8 42.6569 8 41V39Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M60 39C60 37.3431 61.3431 36 63 36H83C84.6569 36 86 37.3431 86 39V41C86 42.6569 84.6569 44 83 44H63C61.3431 44 60 42.6569 60 41V39Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M8 51C8 49.3431 9.34315 48 11 48H31C32.6569 48 34 49.3431 34 51V53C34 54.6569 32.6569 56 31 56H11C9.34315 56 8 54.6569 8 53V51Z" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
7
public/static/images/form/markdown_text_only.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="94" height="48" viewBox="0 0 94 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 11C0 9.34315 1.34315 8 3 8H23C24.6569 8 26 9.34315 26 11V13C26 14.6569 24.6569 16 23 16H3C1.34315 16 0 14.6569 0 13V11Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M30 11C30 9.34315 31.3431 8 33 8H91C92.6569 8 94 9.34315 94 11V13C94 14.6569 92.6569 16 91 16H33C31.3431 16 30 14.6569 30 13V11Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M0 23C0 21.3431 1.34315 20 3 20H61C62.6569 20 64 21.3431 64 23V25C64 26.6569 62.6569 28 61 28H3C1.34315 28 0 26.6569 0 25V23Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M68 23C68 21.3431 69.3431 20 71 20H91C92.6569 20 94 21.3431 94 23V25C94 26.6569 92.6569 28 91 28H71C69.3431 28 68 26.6569 68 25V23Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M0 35C0 33.3431 1.34315 32 3 32H23C24.6569 32 26 33.3431 26 35V37C26 38.6569 24.6569 40 23 40H3C1.34315 40 0 38.6569 0 37V35Z" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 964 B |
7
public/static/images/form/markdown_text_only_dark.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="94" height="48" viewBox="0 0 94 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 11C0 9.34315 1.34315 8 3 8H23C24.6569 8 26 9.34315 26 11V13C26 14.6569 24.6569 16 23 16H3C1.34315 16 0 14.6569 0 13V11Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M30 11C30 9.34315 31.3431 8 33 8H91C92.6569 8 94 9.34315 94 11V13C94 14.6569 92.6569 16 91 16H33C31.3431 16 30 14.6569 30 13V11Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M0 23C0 21.3431 1.34315 20 3 20H61C62.6569 20 64 21.3431 64 23V25C64 26.6569 62.6569 28 61 28H3C1.34315 28 0 26.6569 0 25V23Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M68 23C68 21.3431 69.3431 20 71 20H91C92.6569 20 94 21.3431 94 23V25C94 26.6569 92.6569 28 91 28H71C69.3431 28 68 26.6569 68 25V23Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M0 35C0 33.3431 1.34315 32 3 32H23C24.6569 32 26 33.3431 26 35V37C26 38.6569 24.6569 40 23 40H3C1.34315 40 0 38.6569 0 37V35Z" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 964 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="94" height="40" viewBox="0 0 94 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="40" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="39" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<circle cx="20" cy="20" r="12" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M40 14C40 10.6863 42.6863 8 46 8H65C68.3137 8 71 10.6863 71 14C71 17.3137 68.3137 20 65 20H46C42.6863 20 40 17.3137 40 14Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M40 28C40 25.7909 41.7909 24 44 24H77C79.2091 24 81 25.7909 81 28C81 30.2091 79.2091 32 77 32H44C41.7909 32 40 30.2091 40 28Z" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 652 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="94" height="40" viewBox="0 0 94 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="40" rx="8" fill="black"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="39" rx="7.5" stroke="white" stroke-opacity="0.24"/>
|
||||
<circle cx="20" cy="20" r="12" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M40 14C40 10.6863 42.6863 8 46 8H65C68.3137 8 71 10.6863 71 14C71 17.3137 68.3137 20 65 20H46C42.6863 20 40 17.3137 40 14Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M40 28C40 25.7909 41.7909 24 44 24H77C79.2091 24 81 25.7909 81 28C81 30.2091 79.2091 32 77 32H44C41.7909 32 40 30.2091 40 28Z" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 652 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="72" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="71" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<circle cx="47" cy="20" r="12" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M31.5 46C31.5 42.6863 34.1863 40 37.5 40H56.5C59.8137 40 62.5 42.6863 62.5 46C62.5 49.3137 59.8137 52 56.5 52H37.5C34.1863 52 31.5 49.3137 31.5 46Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M26.5 60C26.5 57.7909 28.2909 56 30.5 56H63.5C65.7091 56 67.5 57.7909 67.5 60C67.5 62.2091 65.7091 64 63.5 64H30.5C28.2909 64 26.5 62.2091 26.5 60Z" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 699 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="72" rx="8" fill="black"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="71" rx="7.5" stroke="white" stroke-opacity="0.24"/>
|
||||
<circle cx="47" cy="20" r="12" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M31.5 46C31.5 42.6863 34.1863 40 37.5 40H56.5C59.8137 40 62.5 42.6863 62.5 46C62.5 49.3137 59.8137 52 56.5 52H37.5C34.1863 52 31.5 49.3137 31.5 46Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M26.5 60C26.5 57.7909 28.2909 56 30.5 56H63.5C65.7091 56 67.5 57.7909 67.5 60C67.5 62.2091 65.7091 64 63.5 64H30.5C28.2909 64 26.5 62.2091 26.5 60Z" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 699 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="94" height="48" viewBox="0 0 94 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="48" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="47" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<rect x="8" y="8" width="78" height="12" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="8" y="28" width="78" height="12" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 414 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="94" height="48" viewBox="0 0 94 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="48" rx="8" fill="black"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="47" rx="7.5" stroke="white" stroke-opacity="0.24"/>
|
||||
<rect x="8" y="8" width="78" height="12" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="8" y="28" width="78" height="12" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 414 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="94" height="28" viewBox="0 0 94 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="28" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="27" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<rect x="8" y="8" width="35" height="12" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="51" y="8" width="35" height="12" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 414 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="94" height="28" viewBox="0 0 94 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="28" rx="8" fill="black"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="27" rx="7.5" stroke="white" stroke-opacity="0.24"/>
|
||||
<rect x="8" y="8" width="35" height="12" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="51" y="8" width="35" height="12" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 414 B |
@@ -32,14 +32,6 @@ export const setupLeafletMap = async (
|
||||
markerClusterStyle.setAttribute("rel", "stylesheet");
|
||||
mapElement.parentNode.appendChild(markerClusterStyle);
|
||||
|
||||
const defaultMarkerClusterStyle = document.createElement("link");
|
||||
defaultMarkerClusterStyle.setAttribute(
|
||||
"href",
|
||||
"/static/images/leaflet/MarkerCluster.Default.css"
|
||||
);
|
||||
defaultMarkerClusterStyle.setAttribute("rel", "stylesheet");
|
||||
mapElement.parentNode.appendChild(defaultMarkerClusterStyle);
|
||||
|
||||
map.setView([52.3731339, 4.8903147], 13);
|
||||
|
||||
const tileLayer = createTileLayer(Leaflet).addTo(map);
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const computeDomain = (entityId: string): string =>
|
||||
entityId.substr(0, entityId.indexOf("."));
|
||||
entityId.substring(0, entityId.indexOf("."));
|
||||
|
||||
@@ -120,11 +120,6 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
return value;
|
||||
}
|
||||
|
||||
if (domain === "datetime") {
|
||||
const time = new Date(state);
|
||||
return formatDateTime(time, locale, config);
|
||||
}
|
||||
|
||||
if (["date", "input_datetime", "time"].includes(domain)) {
|
||||
// If trying to display an explicit state, need to parse the explicit state to `Date` then format.
|
||||
// Attributes aren't available, we have to use `state`.
|
||||
@@ -181,6 +176,7 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
"tag",
|
||||
"tts",
|
||||
"wake_word",
|
||||
"datetime",
|
||||
].includes(domain) ||
|
||||
(domain === "sensor" && attributes.device_class === "timestamp")
|
||||
) {
|
||||
|
||||
@@ -521,14 +521,14 @@ export class HaChartBase extends LitElement {
|
||||
0
|
||||
);
|
||||
if (dataSize > 10000) {
|
||||
// for large datasets zr.flush takes 30-40% of the render time
|
||||
// so we delay it a bit to avoid blocking the main thread
|
||||
// delay the last bit of the render to avoid blocking the main thread
|
||||
// this is not that impactful with sampling enabled but it doesn't hurt to have it
|
||||
const zr = this.chart.getZr();
|
||||
this._originalZrFlush = zr.flush.bind(zr);
|
||||
this._originalZrFlush = zr.flush;
|
||||
zr.flush = () => {
|
||||
setTimeout(() => {
|
||||
this._originalZrFlush?.();
|
||||
}, 10);
|
||||
this._originalZrFlush?.call(zr);
|
||||
}, 5);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,9 +354,10 @@ export class StateHistoryChartLine extends LitElement {
|
||||
name: nameY,
|
||||
color,
|
||||
symbol: "circle",
|
||||
step: "end",
|
||||
animationDurationUpdate: 0,
|
||||
symbolSize: 1,
|
||||
step: "end",
|
||||
sampling: "minmax",
|
||||
animationDurationUpdate: 0,
|
||||
lineStyle: {
|
||||
width: fill ? 0 : 1.5,
|
||||
},
|
||||
|
||||
@@ -492,8 +492,8 @@ export class StatisticsChart extends LitElement {
|
||||
: this.hass.localize(
|
||||
`ui.components.statistics_charts.statistic_types.${type}`
|
||||
),
|
||||
symbol: "circle",
|
||||
symbolSize: 0,
|
||||
symbol: "none",
|
||||
sampling: "minmax",
|
||||
animationDurationUpdate: 0,
|
||||
lineStyle: {
|
||||
width: 1.5,
|
||||
@@ -511,7 +511,6 @@ export class StatisticsChart extends LitElement {
|
||||
if (band && this.chartType === "line") {
|
||||
series.stack = `band-${statistic_id}`;
|
||||
series.stackStrategy = "all";
|
||||
(series as LineSeriesOption).symbol = "none";
|
||||
if (drawBands && type === "max") {
|
||||
(series as LineSeriesOption).areaStyle = {
|
||||
color: color + "3F",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
@@ -8,6 +7,7 @@ import type { Analytics, AnalyticsPreferences } from "../data/analytics";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import "./ha-settings-row";
|
||||
import "./ha-switch";
|
||||
import "./ha-tooltip";
|
||||
import type { HaSwitch } from "./ha-switch";
|
||||
|
||||
const ADDITIONAL_PREFERENCES = ["usage", "statistics"] as const;
|
||||
@@ -67,22 +67,21 @@ export class HaAnalytics extends LitElement {
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
<ha-switch
|
||||
@change=${this._handleRowClick}
|
||||
.checked=${this.analytics?.preferences[preference]}
|
||||
.preference=${preference}
|
||||
name=${preference}
|
||||
<ha-tooltip
|
||||
content=${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
|
||||
)}
|
||||
placement="right"
|
||||
?disabled=${baseEnabled}
|
||||
>
|
||||
</ha-switch>
|
||||
${!baseEnabled
|
||||
? html`
|
||||
<simple-tooltip animation-delay="0" position="right">
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
|
||||
)}
|
||||
</simple-tooltip>
|
||||
`
|
||||
: ""}
|
||||
<ha-switch
|
||||
@change=${this._handleRowClick}
|
||||
.checked=${this.analytics?.preferences[preference]}
|
||||
.preference=${preference}
|
||||
name=${preference}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-tooltip>
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
`
|
||||
|
||||
110
src/components/ha-copy-textfield.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { mdiContentCopy, mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
|
||||
import "./ha-button";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { copyToClipboard } from "../common/util/copy-clipboard";
|
||||
import { showToast } from "../util/toast";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@customElement("ha-copy-textfield")
|
||||
export class HaCopyTextfield extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "value" }) public value!: string;
|
||||
|
||||
@property({ attribute: "masked-value" }) public maskedValue?: string;
|
||||
|
||||
@property({ attribute: "label" }) public label?: string;
|
||||
|
||||
@state() private _showMasked = true;
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="textfield-container">
|
||||
<ha-textfield
|
||||
.value=${this._showMasked && this.maskedValue
|
||||
? this.maskedValue
|
||||
: this.value}
|
||||
readonly
|
||||
.suffix=${this.maskedValue
|
||||
? html`<div style="width: 24px"></div>`
|
||||
: nothing}
|
||||
@click=${this._focusInput}
|
||||
></ha-textfield>
|
||||
${this.maskedValue
|
||||
? html`<ha-icon-button
|
||||
class="toggle-unmasked"
|
||||
.label=${this.hass.localize(
|
||||
`ui.common.${this._showMasked ? "show" : "hide"}`
|
||||
)}
|
||||
@click=${this._toggleMasked}
|
||||
.path=${this._showMasked ? mdiEye : mdiEyeOff}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-button @click=${this._copy} unelevated>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
${this.label || this.hass.localize("ui.common.copy")}
|
||||
</ha-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _focusInput(ev) {
|
||||
const inputElement = ev.currentTarget as HaTextField;
|
||||
inputElement.select();
|
||||
}
|
||||
|
||||
private _toggleMasked(): void {
|
||||
this._showMasked = !this._showMasked;
|
||||
}
|
||||
|
||||
private async _copy(): Promise<void> {
|
||||
await copyToClipboard(this.value);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.textfield-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.textfield-container ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.toggle-unmasked {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-copy-textfield": HaCopyTextfield;
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,6 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
if (!this.isPassword) return nothing;
|
||||
return html`
|
||||
<ha-icon-button
|
||||
toggles
|
||||
.label=${this.localize?.(
|
||||
`${this.localizeBaseKey}.${
|
||||
this.unmaskedPassword ? "hide_password" : "show_password"
|
||||
|
||||
@@ -132,7 +132,6 @@ export class HaPasswordField extends LitElement {
|
||||
@change=${this._handleChangeEvent}
|
||||
></ha-textfield>
|
||||
<ha-icon-button
|
||||
toggles
|
||||
.label=${this.hass?.localize(
|
||||
this._unmaskedPassword
|
||||
? "ui.components.selectors.text.hide_password"
|
||||
|
||||
193
src/components/ha-select-box.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import "./ha-radio";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import type { HaRadio } from "./ha-radio";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
export interface SelectBoxOption {
|
||||
label?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@customElement("ha-select-box")
|
||||
export class HaSelectBox extends LitElement {
|
||||
@property({ attribute: false }) public options: SelectBoxOption[] = [];
|
||||
|
||||
@property({ attribute: false }) public value?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled?: boolean;
|
||||
|
||||
@property({ type: Number, attribute: "max_columns" })
|
||||
public maxColumns?: number;
|
||||
|
||||
render() {
|
||||
const maxColumns = this.maxColumns ?? 3;
|
||||
const columns = Math.min(maxColumns, this.options.length);
|
||||
|
||||
return html`
|
||||
<div class="list" style=${styleMap({ "--columns": columns })}>
|
||||
${this.options.map((option) => this._renderOption(option))}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderOption(option: SelectBoxOption) {
|
||||
const horizontal = this.maxColumns === 1;
|
||||
const disabled = option.disabled || this.disabled || false;
|
||||
const selected = option.value === this.value;
|
||||
return html`
|
||||
<label
|
||||
class="option ${classMap({
|
||||
horizontal: horizontal,
|
||||
selected: selected,
|
||||
})}"
|
||||
?disabled=${disabled}
|
||||
@click=${this._labelClick}
|
||||
>
|
||||
<div class="content">
|
||||
<ha-radio
|
||||
.checked=${option.value === this.value}
|
||||
.value=${option.value}
|
||||
.disabled=${disabled}
|
||||
@change=${this._radioChanged}
|
||||
></ha-radio>
|
||||
<div class="text">
|
||||
<span class="label">${option.label}</span>
|
||||
${option.description
|
||||
? html`<span class="description">${option.description}</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
${option.image ? html`<img alt="" src=${option.image} />` : nothing}
|
||||
</label>
|
||||
`;
|
||||
}
|
||||
|
||||
private _labelClick(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.currentTarget.querySelector("ha-radio")?.click();
|
||||
}
|
||||
|
||||
private _radioChanged(ev: CustomEvent) {
|
||||
const radio = ev.currentTarget as HaRadio;
|
||||
const value = radio.value;
|
||||
if (this.disabled || value === undefined || value === (this.value ?? "")) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--columns, 1), minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.option {
|
||||
position: relative;
|
||||
display: block;
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.option .content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.option .content ha-radio {
|
||||
margin: -12px;
|
||||
flex: none;
|
||||
}
|
||||
.option .content .text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.option .content .text .label {
|
||||
color: var(--primary-text-color);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.option .content .text .description {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
}
|
||||
img {
|
||||
position: relative;
|
||||
max-width: var(--ha-select-box-image-size, 96px);
|
||||
max-height: var(--ha-select-box-image-size, 96px);
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.option.horizontal {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.option.horizontal img {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.option:before {
|
||||
content: "";
|
||||
display: block;
|
||||
inset: 0;
|
||||
position: absolute;
|
||||
background-color: transparent;
|
||||
pointer-events: none;
|
||||
opacity: 0.2;
|
||||
transition:
|
||||
background-color 180ms ease-in-out,
|
||||
opacity 180ms ease-in-out;
|
||||
}
|
||||
.option:hover:before {
|
||||
background-color: var(--divider-color);
|
||||
}
|
||||
.option.selected:before {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
.option[disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.option[disabled] .content,
|
||||
.option[disabled] img {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.option[disabled]:before {
|
||||
background-color: var(--disabled-color);
|
||||
opacity: 0.05;
|
||||
}
|
||||
`;
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-select-box": HaSelectBox;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import "../ha-input-helper-text";
|
||||
import "../ha-radio";
|
||||
import "../ha-select";
|
||||
import "../ha-sortable";
|
||||
import "../ha-select-box";
|
||||
|
||||
@customElement("ha-selector-select")
|
||||
export class HaSelectSelector extends LitElement {
|
||||
@@ -91,6 +92,24 @@ export class HaSelectSelector extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!this.selector.select?.multiple &&
|
||||
!this.selector.select?.reorder &&
|
||||
!this.selector.select?.custom_value &&
|
||||
this._mode === "box"
|
||||
) {
|
||||
return html`
|
||||
${this.label ? html`<span class="label">${this.label}</span>` : nothing}
|
||||
<ha-select-box
|
||||
.options=${options}
|
||||
.value=${this.value as string | undefined}
|
||||
@value-changed=${this._valueChanged}
|
||||
.maxColumns=${this.selector.select?.box_max_columns}
|
||||
></ha-select-box>
|
||||
${this._renderHelper()}
|
||||
`;
|
||||
}
|
||||
|
||||
if (
|
||||
!this.selector.select?.custom_value &&
|
||||
!this.selector.select?.reorder &&
|
||||
@@ -269,7 +288,7 @@ export class HaSelectSelector extends LitElement {
|
||||
: "";
|
||||
}
|
||||
|
||||
private get _mode(): "list" | "dropdown" {
|
||||
private get _mode(): "list" | "dropdown" | "box" {
|
||||
return (
|
||||
this.selector.select?.mode ||
|
||||
((this.selector.select?.options?.length || 0) < 6 ? "list" : "dropdown")
|
||||
@@ -411,6 +430,15 @@ export class HaSelectSelector extends LitElement {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
ha-select-box + ha-input-helper-text {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.sortable-fallback {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
|
||||
@@ -95,7 +95,6 @@ export class HaTextSelector extends LitElement {
|
||||
></ha-textfield>
|
||||
${this.selector.text?.type === "password"
|
||||
? html`<ha-icon-button
|
||||
toggles
|
||||
.label=${this.hass?.localize(
|
||||
this._unmaskedPassword
|
||||
? "ui.components.selectors.text.hide_password"
|
||||
|
||||
41
src/components/ha-tooltip.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import SlTooltip from "@shoelace-style/shoelace/dist/components/tooltip/tooltip.component";
|
||||
import styles from "@shoelace-style/shoelace/dist/components/tooltip/tooltip.styles";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { setDefaultAnimation } from "@shoelace-style/shoelace/dist/utilities/animation-registry";
|
||||
|
||||
setDefaultAnimation("tooltip.show", {
|
||||
keyframes: [{ opacity: 0 }, { opacity: 1 }],
|
||||
options: { duration: 150, easing: "ease" },
|
||||
});
|
||||
|
||||
setDefaultAnimation("tooltip.hide", {
|
||||
keyframes: [{ opacity: 1 }, { opacity: 0 }],
|
||||
options: { duration: 400, easing: "ease" },
|
||||
});
|
||||
|
||||
@customElement("ha-tooltip")
|
||||
export class HaTooltip extends SlTooltip {
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--sl-tooltip-background-color: var(--secondary-background-color);
|
||||
--sl-tooltip-color: var(--primary-text-color);
|
||||
--sl-tooltip-font-family: Roboto, sans-serif;
|
||||
--sl-tooltip-font-size: 12px;
|
||||
--sl-tooltip-font-weight: normal;
|
||||
--sl-tooltip-line-height: 1;
|
||||
--sl-tooltip-padding: 8px;
|
||||
--sl-tooltip-border-radius: var(--ha-tooltip-border-radius, 4px);
|
||||
--sl-tooltip-arrow-size: var(--ha-tooltip-arrow-size, 8px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-tooltip": HaTooltip;
|
||||
}
|
||||
}
|
||||
@@ -581,6 +581,7 @@ export class HaMap extends ReactiveElement {
|
||||
this._mapCluster = Leaflet.markerClusterGroup({
|
||||
showCoverageOnHover: false,
|
||||
removeOutsideVisibleBounds: false,
|
||||
maxClusterRadius: 40,
|
||||
});
|
||||
this._mapCluster.addLayers(this._mapItems);
|
||||
map.addLayer(this._mapCluster);
|
||||
@@ -672,6 +673,22 @@ export class HaMap extends ReactiveElement {
|
||||
box-shadow: none !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.marker-cluster div {
|
||||
background-clip: padding-box;
|
||||
background-color: var(--primary-color);
|
||||
border: 3px solid rgba(var(--rgb-primary-color), 0.2);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 20px;
|
||||
text-align: center;
|
||||
color: var(--text-primary-color);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.marker-cluster span {
|
||||
line-height: 30px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ export interface CloudWebhook {
|
||||
interface CloudLoginBase {
|
||||
hass: HomeAssistant;
|
||||
email: string;
|
||||
check_connection?: boolean;
|
||||
}
|
||||
|
||||
export interface CloudLoginPassword extends CloudLoginBase {
|
||||
|
||||
@@ -346,6 +346,8 @@ export interface AssistPipelineSelector {
|
||||
export interface SelectOption {
|
||||
value: any;
|
||||
label: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -353,11 +355,12 @@ export interface SelectSelector {
|
||||
select: {
|
||||
multiple?: boolean;
|
||||
custom_value?: boolean;
|
||||
mode?: "list" | "dropdown";
|
||||
mode?: "list" | "dropdown" | "box";
|
||||
options: readonly string[] | readonly SelectOption[];
|
||||
translation_key?: string;
|
||||
sort?: boolean;
|
||||
reorder?: boolean;
|
||||
box_max_columns?: number;
|
||||
} | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,8 +40,13 @@ export class DialogEnterCode
|
||||
|
||||
@state() private _showClearButton = false;
|
||||
|
||||
@state() private _narrow = false;
|
||||
|
||||
public async showDialog(dialogParams: EnterCodeDialogParams): Promise<void> {
|
||||
this._dialogParams = dialogParams;
|
||||
this._narrow = matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
@@ -96,7 +101,7 @@ export class DialogEnterCode
|
||||
>
|
||||
<ha-textfield
|
||||
class="input"
|
||||
dialogInitialFocus
|
||||
?dialogInitialFocus=${!this._narrow}
|
||||
id="code"
|
||||
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
|
||||
type="password"
|
||||
@@ -134,6 +139,7 @@ export class DialogEnterCode
|
||||
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
|
||||
type="password"
|
||||
inputmode="numeric"
|
||||
?dialogInitialFocus=${!this._narrow}
|
||||
></ha-textfield>
|
||||
<div class="keypad">
|
||||
${BUTTONS.map((value) =>
|
||||
|
||||
@@ -99,7 +99,12 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
this._stateObj.attributes.available_tones
|
||||
).map(
|
||||
([toneId, toneName]) => html`
|
||||
<ha-list-item .value=${toneId}
|
||||
<ha-list-item
|
||||
.value=${Array.isArray(
|
||||
this._stateObj!.attributes.available_tones
|
||||
)
|
||||
? toneName
|
||||
: toneId}
|
||||
>${toneName}</ha-list-item
|
||||
>
|
||||
`
|
||||
@@ -179,7 +184,7 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
await this.hass.callService("siren", "turn_on", {
|
||||
entity_id: this._stateObj!.entity_id,
|
||||
tone: this._tone,
|
||||
volume: this._volume,
|
||||
volume_level: this._volume,
|
||||
duration: this._duration,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import "@material/mwc-button";
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { formatDateTime } from "../../common/datetime/format_date_time";
|
||||
import "../../components/ha-markdown";
|
||||
import "../../components/ha-relative-time";
|
||||
import "../../components/ha-tooltip";
|
||||
import "../../components/ha-button";
|
||||
import type { PersistentNotification } from "../../data/persistent_notification";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./notification-item-template";
|
||||
@@ -28,21 +28,23 @@ export class HuiPersistentNotificationItem extends LitElement {
|
||||
|
||||
<div class="time">
|
||||
<span>
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.notification.created_at}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
<simple-tooltip animation-delay="0">
|
||||
${this._computeTooltip(this.hass, this.notification)}
|
||||
</simple-tooltip>
|
||||
<ha-tooltip
|
||||
.content=${this._computeTooltip(this.hass, this.notification)}
|
||||
placement="bottom"
|
||||
>
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.notification.created_at}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
</ha-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<mwc-button slot="actions" @click=${this._handleDismiss}
|
||||
<ha-button slot="actions" @click=${this._handleDismiss}
|
||||
>${this.hass.localize(
|
||||
"ui.card.persistent_notification.dismiss"
|
||||
)}</mwc-button
|
||||
)}</ha-button
|
||||
>
|
||||
</notification-item-template>
|
||||
`;
|
||||
|
||||
@@ -251,6 +251,7 @@ export class QuickBar extends LitElement {
|
||||
<mwc-list>
|
||||
${this._opened
|
||||
? html`<lit-virtualizer
|
||||
tabindex="-1"
|
||||
scroller
|
||||
@keydown=${this._handleListItemKeyDown}
|
||||
@rangechange=${this._handleRangeChanged}
|
||||
@@ -326,6 +327,7 @@ export class QuickBar extends LitElement {
|
||||
.twoline=${Boolean(item.area)}
|
||||
.item=${item}
|
||||
index=${ifDefined(index)}
|
||||
tabindex="0"
|
||||
>
|
||||
<span>${item.primaryText}</span>
|
||||
${item.area
|
||||
@@ -346,6 +348,7 @@ export class QuickBar extends LitElement {
|
||||
.item=${item}
|
||||
index=${ifDefined(index)}
|
||||
graphic="icon"
|
||||
tabindex="0"
|
||||
>
|
||||
${item.iconPath
|
||||
? html`
|
||||
@@ -375,6 +378,7 @@ export class QuickBar extends LitElement {
|
||||
index=${ifDefined(index)}
|
||||
class="command-item"
|
||||
hasMeta
|
||||
tabindex="0"
|
||||
>
|
||||
<span>
|
||||
<ha-label
|
||||
|
||||
@@ -10,6 +10,7 @@ import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../components/ha-textfield";
|
||||
import { cloudLogin } from "../../../data/cloud";
|
||||
import { showCloudAlreadyConnectedDialog } from "../../../panels/config/cloud/dialog-cloud-already-connected/show-dialog-cloud-already-connected";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@@ -25,6 +26,8 @@ export class CloudStepSignin extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _checkConnection = true;
|
||||
|
||||
@query("#email", true) private _emailField!: HaTextField;
|
||||
|
||||
@query("#password", true) private _passwordField!: HaPasswordField;
|
||||
@@ -115,6 +118,7 @@ export class CloudStepSignin extends LitElement {
|
||||
hass: this.hass,
|
||||
email: username,
|
||||
...(code ? { code } : { password }),
|
||||
check_connection: this._checkConnection,
|
||||
});
|
||||
} catch (err: any) {
|
||||
const errCode = err && err.body && err.body.code;
|
||||
@@ -139,6 +143,20 @@ export class CloudStepSignin extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
if (errCode === "alreadyconnectederror") {
|
||||
showCloudAlreadyConnectedDialog(this, {
|
||||
details: JSON.parse(err.body.message),
|
||||
logInHereAction: () => {
|
||||
this._checkConnection = false;
|
||||
doLogin(username);
|
||||
},
|
||||
closeDialog: () => {
|
||||
this._requestInProgress = false;
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
|
||||
await doLogin(username.toLowerCase());
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import { isNavigationClick } from "../common/dom/is-navigation-click";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { getStorageDefaultPanelUrlPath } from "../data/panel";
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
} from "../util/register-service-worker";
|
||||
import "./ha-init-page";
|
||||
import "./home-assistant-main";
|
||||
import { storage } from "../common/decorators/storage";
|
||||
|
||||
const useHash = __DEMO__;
|
||||
const curPath = () =>
|
||||
@@ -40,6 +42,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
|
||||
private _panelUrl: string;
|
||||
|
||||
@storage({ key: "ha-version", state: false, subscribe: false })
|
||||
private _haVersion?: string;
|
||||
|
||||
private _hiddenTimeout?: number;
|
||||
@@ -182,9 +185,15 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
|
||||
protected hassReconnected() {
|
||||
super.hassReconnected();
|
||||
this._checkUpdate(this.hass!.connection);
|
||||
}
|
||||
|
||||
private _checkUpdate(connection: Connection) {
|
||||
const oldVersion = this._haVersion;
|
||||
const currentVersion = connection.haVersion;
|
||||
// If backend has been upgraded, make sure we update frontend
|
||||
if (this.hass!.connection.haVersion !== this._haVersion) {
|
||||
if (currentVersion !== oldVersion) {
|
||||
this._haVersion = currentVersion;
|
||||
if (supportsServiceWorker()) {
|
||||
navigator.serviceWorker.getRegistration().then((registration) => {
|
||||
if (registration) {
|
||||
@@ -244,7 +253,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
}
|
||||
|
||||
const { auth, conn } = result;
|
||||
this._haVersion = conn.haVersion;
|
||||
this._checkUpdate(conn);
|
||||
this.initializeHass(auth, conn);
|
||||
} catch (_err: any) {
|
||||
this._renderInitInfo(true);
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { mdiContentCopy, mdiEye, mdiEyeOff, mdiHelpCircle } from "@mdi/js";
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-radio";
|
||||
import "../../../../components/ha-settings-row";
|
||||
import "../../../../components/ha-switch";
|
||||
import "../../../../components/ha-textfield";
|
||||
|
||||
import { formatDate } from "../../../../common/datetime/format_date";
|
||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
||||
@@ -25,6 +21,7 @@ import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import { showCloudCertificateDialog } from "../dialog-cloud-certificate/show-dialog-cloud-certificate";
|
||||
import { obfuscateUrl } from "../../../../util/url";
|
||||
import "../../../../components/ha-copy-textfield";
|
||||
|
||||
@customElement("cloud-remote-pref")
|
||||
export class CloudRemotePref extends LitElement {
|
||||
@@ -34,8 +31,6 @@ export class CloudRemotePref extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _unmaskedUrl = false;
|
||||
|
||||
protected render() {
|
||||
if (!this.cloudStatus) {
|
||||
return nothing;
|
||||
@@ -139,37 +134,13 @@ export class CloudRemotePref extends LitElement {
|
||||
)}
|
||||
</p>
|
||||
`}
|
||||
<div class="url-container">
|
||||
<div class="textfield-container">
|
||||
<ha-textfield
|
||||
.value=${this._unmaskedUrl
|
||||
? `https://${remote_domain}`
|
||||
: obfuscateUrl(`https://${remote_domain}`)}
|
||||
readonly
|
||||
.suffix=${
|
||||
// reserve some space for the icon.
|
||||
html`<div style="width: 24px"></div>`
|
||||
}
|
||||
></ha-textfield>
|
||||
<ha-icon-button
|
||||
class="toggle-unmasked-url"
|
||||
toggles
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.common.${this._unmaskedUrl ? "hide" : "show"}_url`
|
||||
)}
|
||||
@click=${this._toggleUnmaskedUrl}
|
||||
.path=${this._unmaskedUrl ? mdiEyeOff : mdiEye}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<ha-button
|
||||
.url=${`https://${remote_domain}`}
|
||||
@click=${this._copyURL}
|
||||
unelevated
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.common.copy_link")}
|
||||
</ha-button>
|
||||
</div>
|
||||
|
||||
<ha-copy-textfield
|
||||
.hass=${this.hass}
|
||||
.value=${`https://${remote_domain}`}
|
||||
.maskedValue=${obfuscateUrl(`https://${remote_domain}`)}
|
||||
.label=${this.hass!.localize("ui.panel.config.common.copy_link")}
|
||||
></ha-copy-textfield>
|
||||
|
||||
<ha-expansion-panel
|
||||
outlined
|
||||
@@ -234,10 +205,6 @@ export class CloudRemotePref extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _toggleUnmaskedUrl(): void {
|
||||
this._unmaskedUrl = !this._unmaskedUrl;
|
||||
}
|
||||
|
||||
private async _toggleChanged(ev) {
|
||||
const toggle = ev.target as HaSwitch;
|
||||
|
||||
@@ -268,14 +235,6 @@ export class CloudRemotePref extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _copyURL(ev): Promise<void> {
|
||||
const url = ev.currentTarget.url;
|
||||
await copyToClipboard(url);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.preparing {
|
||||
padding: 0 16px 16px;
|
||||
@@ -335,30 +294,6 @@ export class CloudRemotePref extends LitElement {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.url-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.textfield-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
.textfield-container ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
.toggle-unmasked-url {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
hr {
|
||||
border: none;
|
||||
height: 1px;
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import { formatDateTime } from "../../../../common/datetime/format_date_time";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { CloudAlreadyConnectedParams as CloudAlreadyConnectedDialogParams } from "./show-dialog-cloud-already-connected";
|
||||
import { obfuscateUrl } from "../../../../util/url";
|
||||
|
||||
@customElement("dialog-cloud-already-connected")
|
||||
class DialogCloudAlreadyConnected extends LitElement {
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: CloudAlreadyConnectedDialogParams;
|
||||
|
||||
@state() private _obfuscateIp = true;
|
||||
|
||||
public showDialog(params: CloudAlreadyConnectedDialogParams) {
|
||||
this._params = params;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._params?.closeDialog();
|
||||
this._params = undefined;
|
||||
this._obfuscateIp = true;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
const { details } = this._params;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize(
|
||||
"ui.panel.config.cloud.dialog_already_connected.heading"
|
||||
)
|
||||
)}
|
||||
>
|
||||
<div class="intro">
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.dialog_already_connected.description"
|
||||
)}
|
||||
</span>
|
||||
<b>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.dialog_already_connected.other_home_assistant"
|
||||
)}
|
||||
</b>
|
||||
</div>
|
||||
<div class="instance-details">
|
||||
<div class="instance-detail">
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.dialog_already_connected.ip_address"
|
||||
)}:
|
||||
</span>
|
||||
<div class="obfuscated">
|
||||
<span>
|
||||
${this._obfuscateIp
|
||||
? obfuscateUrl(details.remote_ip_address)
|
||||
: details.remote_ip_address}
|
||||
</span>
|
||||
|
||||
<ha-icon-button
|
||||
class="toggle-unmasked-url"
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.cloud.dialog_already_connected.obfuscated_ip.${this._obfuscateIp ? "hide" : "show"}`
|
||||
)}
|
||||
@click=${this._toggleObfuscateIp}
|
||||
.path=${this._obfuscateIp ? mdiEye : mdiEyeOff}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="instance-detail">
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.dialog_already_connected.connected_at"
|
||||
)}:
|
||||
</span>
|
||||
<span>
|
||||
${formatDateTime(
|
||||
new Date(details.connected_at),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ha-alert
|
||||
alert-type="info"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.cloud.dialog_already_connected.info_backups.title"
|
||||
)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.dialog_already_connected.info_backups.description"
|
||||
)}
|
||||
</ha-alert>
|
||||
|
||||
<ha-button @click=${this.closeDialog} slot="secondaryAction">
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button @click=${this._logInHere} slot="primaryAction">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.cloud.dialog_already_connected.login_here"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _toggleObfuscateIp() {
|
||||
this._obfuscateIp = !this._obfuscateIp;
|
||||
}
|
||||
|
||||
private _logInHere() {
|
||||
this._params?.logInHereAction();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 535px;
|
||||
}
|
||||
.intro b {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.instance-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.instance-detail {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.obfuscated {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-cloud-already-connected": DialogCloudAlreadyConnected;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
|
||||
export interface CloudAlreadyConnectedParams {
|
||||
details: {
|
||||
remote_ip_address: string;
|
||||
connected_at: string;
|
||||
};
|
||||
logInHereAction: () => void;
|
||||
closeDialog: () => void;
|
||||
}
|
||||
|
||||
export const showCloudAlreadyConnectedDialog = (
|
||||
element: HTMLElement,
|
||||
webhookDialogParams: CloudAlreadyConnectedParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-cloud-already-connected",
|
||||
dialogImport: () => import("./dialog-cloud-already-connected"),
|
||||
dialogParams: webhookDialogParams,
|
||||
});
|
||||
};
|
||||
@@ -1,27 +1,23 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiContentCopy, mdiOpenInNew } from "@mdi/js";
|
||||
import { mdiOpenInNew } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { query, state } from "lit/decorators";
|
||||
import { state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../../components/ha-textfield";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { documentationUrl } from "../../../../util/documentation-url";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import type { WebhookDialogParams } from "./show-dialog-manage-cloudhook";
|
||||
|
||||
import "../../../../components/ha-copy-textfield";
|
||||
|
||||
export class DialogManageCloudhook extends LitElement {
|
||||
protected hass?: HomeAssistant;
|
||||
|
||||
@state() private _params?: WebhookDialogParams;
|
||||
|
||||
@query("ha-textfield") _input!: HaTextField;
|
||||
|
||||
public showDialog(params: WebhookDialogParams) {
|
||||
this._params = params;
|
||||
}
|
||||
@@ -82,21 +78,12 @@ export class DialogManageCloudhook extends LitElement {
|
||||
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</a>
|
||||
</p>
|
||||
<ha-textfield
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.cloud.dialog_cloudhook.public_url"
|
||||
)}
|
||||
|
||||
<ha-copy-textfield
|
||||
.hass=${this.hass}
|
||||
.value=${cloudhook.cloudhook_url}
|
||||
iconTrailing
|
||||
readOnly
|
||||
@click=${this._focusInput}
|
||||
>
|
||||
<ha-icon-button
|
||||
@click=${this._copyUrl}
|
||||
slot="trailingIcon"
|
||||
.path=${mdiContentCopy}
|
||||
></ha-icon-button>
|
||||
</ha-textfield>
|
||||
.label=${this.hass!.localize("ui.panel.config.common.copy_link")}
|
||||
></ha-copy-textfield>
|
||||
</div>
|
||||
|
||||
<a
|
||||
@@ -137,24 +124,6 @@ export class DialogManageCloudhook extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _focusInput(ev) {
|
||||
const inputElement = ev.currentTarget as HaTextField;
|
||||
inputElement.select();
|
||||
}
|
||||
|
||||
private async _copyUrl(ev): Promise<void> {
|
||||
if (!this.hass) return;
|
||||
ev.stopPropagation();
|
||||
const inputElement = ev.target.parentElement as HaTextField;
|
||||
inputElement.select();
|
||||
const url = this.hass.hassUrl(inputElement.value);
|
||||
|
||||
await copyToClipboard(url);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -163,13 +132,6 @@ export class DialogManageCloudhook extends LitElement {
|
||||
ha-dialog {
|
||||
width: 650px;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
ha-textfield > ha-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
button.link {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
|
||||
@@ -28,6 +28,7 @@ import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../../ha-config-section";
|
||||
import { showSupportPackageDialog } from "../account/show-dialog-cloud-support-package";
|
||||
import { showCloudAlreadyConnectedDialog } from "../dialog-cloud-already-connected/show-dialog-cloud-already-connected";
|
||||
|
||||
@customElement("cloud-login")
|
||||
export class CloudLogin extends LitElement {
|
||||
@@ -47,6 +48,8 @@ export class CloudLogin extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _checkConnection = true;
|
||||
|
||||
@query("#email", true) private _emailField!: HaTextField;
|
||||
|
||||
@query("#password", true) private _passwordField!: HaPasswordField;
|
||||
@@ -244,6 +247,7 @@ export class CloudLogin extends LitElement {
|
||||
hass: this.hass,
|
||||
email: username,
|
||||
...(code ? { code } : { password }),
|
||||
check_connection: this._checkConnection,
|
||||
});
|
||||
this.email = "";
|
||||
this._password = "";
|
||||
@@ -283,6 +287,21 @@ export class CloudLogin extends LitElement {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (errCode === "alreadyconnectederror") {
|
||||
showCloudAlreadyConnectedDialog(this, {
|
||||
details: JSON.parse(err.body.message),
|
||||
logInHereAction: () => {
|
||||
this._checkConnection = false;
|
||||
doLogin(username);
|
||||
},
|
||||
closeDialog: () => {
|
||||
this._requestInProgress = false;
|
||||
this.email = "";
|
||||
this._password = "";
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (errCode === "PasswordChangeRequired") {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
|
||||
@@ -185,6 +185,14 @@ class AddIntegrationDialog extends LitElement {
|
||||
const yamlIntegrations: IntegrationListItem[] = [];
|
||||
|
||||
Object.entries(i).forEach(([domain, integration]) => {
|
||||
if (
|
||||
"integration_type" in integration &&
|
||||
integration.integration_type === "hardware"
|
||||
) {
|
||||
// Ignore hardware integrations, they cannot be added via UI
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
"integration_type" in integration &&
|
||||
(integration.config_flow ||
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
} from "../../../../../components/data-table/ha-data-table";
|
||||
import "../../../../../components/ha-fab";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-relative-time";
|
||||
import type {
|
||||
BluetoothDeviceData,
|
||||
BluetoothScannersDetails,
|
||||
@@ -140,6 +141,18 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement {
|
||||
sortable: true,
|
||||
defaultHidden: true,
|
||||
},
|
||||
time: {
|
||||
title: localize("ui.panel.config.bluetooth.last_seen"),
|
||||
filterable: false,
|
||||
sortable: true,
|
||||
defaultHidden: false,
|
||||
template: (ad) =>
|
||||
html`<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${ad.datetime}
|
||||
capitalize
|
||||
></ha-relative-time>`,
|
||||
},
|
||||
rssi: {
|
||||
title: localize("ui.panel.config.bluetooth.rssi"),
|
||||
type: "numeric",
|
||||
@@ -167,6 +180,7 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement {
|
||||
scanner?.name ||
|
||||
row.source,
|
||||
device: device?.name_by_user || device?.name || undefined,
|
||||
datetime: new Date(row.time * 1000),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
@@ -153,7 +153,6 @@ class ConfigUrlForm extends LitElement {
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class="toggle-unmasked-url"
|
||||
toggles
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.common.${this._unmaskedExternalUrl ? "hide" : "show"}_url`
|
||||
)}
|
||||
@@ -254,7 +253,6 @@ class ConfigUrlForm extends LitElement {
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class="toggle-unmasked-url"
|
||||
toggles
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.common.${this._unmaskedInternalUrl ? "hide" : "show"}_url`
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { mdiRestore, mdiPlus, mdiMinus } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import "../../../components/ha-control-select";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||
import { COUNTER_ACTIONS, type CounterActionsCardFeatureConfig } from "./types";
|
||||
import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-control-button";
|
||||
|
||||
export const supportsCounterActionsCardFeature = (stateObj: HassEntity) => {
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return domain === "counter";
|
||||
};
|
||||
|
||||
interface CounterButton {
|
||||
translationKey: string;
|
||||
icon: string;
|
||||
serviceName: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export const COUNTER_ACTIONS_BUTTON: Record<
|
||||
string,
|
||||
(stateObj: HassEntity) => CounterButton
|
||||
> = {
|
||||
increment: (stateObj) => ({
|
||||
translationKey: "increment",
|
||||
icon: mdiPlus,
|
||||
serviceName: "increment",
|
||||
disabled: parseInt(stateObj.state) === stateObj.attributes.maximum,
|
||||
}),
|
||||
reset: () => ({
|
||||
translationKey: "reset",
|
||||
icon: mdiRestore,
|
||||
serviceName: "reset",
|
||||
disabled: false,
|
||||
}),
|
||||
decrement: (stateObj) => ({
|
||||
translationKey: "decrement",
|
||||
icon: mdiMinus,
|
||||
serviceName: "decrement",
|
||||
disabled: parseInt(stateObj.state) === stateObj.attributes.minimum,
|
||||
}),
|
||||
};
|
||||
|
||||
@customElement("hui-counter-actions-card-feature")
|
||||
class HuiCounterActionsCardFeature
|
||||
extends LitElement
|
||||
implements LovelaceCardFeature
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@state() private _config?: CounterActionsCardFeatureConfig;
|
||||
|
||||
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
|
||||
await import(
|
||||
"../editor/config-elements/hui-counter-actions-card-feature-editor"
|
||||
);
|
||||
return document.createElement("hui-counter-actions-card-feature-editor");
|
||||
}
|
||||
|
||||
static getStubConfig(): CounterActionsCardFeatureConfig {
|
||||
return {
|
||||
type: "counter-actions",
|
||||
actions: COUNTER_ACTIONS.map((action) => action),
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: CounterActionsCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | null {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.stateObj ||
|
||||
!supportsCounterActionsCardFeature(this.stateObj)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-control-button-group>
|
||||
${this._config?.actions
|
||||
?.filter((action) => COUNTER_ACTIONS.includes(action))
|
||||
.map((action) => {
|
||||
const button = COUNTER_ACTIONS_BUTTON[action](this.stateObj!);
|
||||
return html`
|
||||
<ha-control-button
|
||||
.entry=${button}
|
||||
.label=${this.hass!.localize(
|
||||
// @ts-ignore
|
||||
`ui.card.counter.actions.${button.translationKey}`
|
||||
)}
|
||||
@click=${this._onActionTap}
|
||||
.disabled=${button.disabled ||
|
||||
this.stateObj?.state === UNAVAILABLE}
|
||||
>
|
||||
<ha-svg-icon .path=${button.icon}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`;
|
||||
})}
|
||||
</ha-control-button-group>
|
||||
`;
|
||||
}
|
||||
|
||||
private _onActionTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
const entry = (ev.target! as any).entry as CounterButton;
|
||||
this.hass!.callService("counter", entry.serviceName, {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = cardFeatureStyles;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-counter-actions-card-feature": HuiCounterActionsCardFeature;
|
||||
}
|
||||
}
|
||||
111
src/panels/lovelace/card-features/hui-toggle-card-feature.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { mdiPowerOff, mdiPower } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import "../../../components/ha-control-select";
|
||||
import type { ControlSelectOption } from "../../../components/ha-control-select";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCardFeature } from "../types";
|
||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||
import type { ToggleCardFeatureConfig } from "./types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
|
||||
export const supportsToggleCardFeature = (stateObj: HassEntity) => {
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return ["switch", "input_boolean"].includes(domain);
|
||||
};
|
||||
|
||||
@customElement("hui-toggle-card-feature")
|
||||
class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@state() private _config?: ToggleCardFeatureConfig;
|
||||
|
||||
static getStubConfig(): ToggleCardFeatureConfig {
|
||||
return {
|
||||
type: "toggle",
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: ToggleCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | null {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.stateObj ||
|
||||
!supportsToggleCardFeature(this.stateObj)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const color = stateColorCss(this.stateObj);
|
||||
|
||||
const options = ["on", "off"].map<ControlSelectOption>((entityState) => ({
|
||||
value: entityState,
|
||||
label: this.hass!.formatEntityState(this.stateObj!, entityState),
|
||||
path: entityState === "on" ? mdiPower : mdiPowerOff,
|
||||
}));
|
||||
|
||||
return html`
|
||||
<ha-control-select
|
||||
.options=${options}
|
||||
.value=${this.stateObj.state}
|
||||
@value-changed=${this._valueChanged}
|
||||
hide-label
|
||||
.ariaLabel=${this.hass.localize("ui.card.humidifier.state")}
|
||||
style=${styleMap({
|
||||
"--control-select-color": color,
|
||||
})}
|
||||
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||
>
|
||||
</ha-control-select>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _valueChanged(ev: CustomEvent) {
|
||||
const newState = (ev.detail as any).value;
|
||||
|
||||
if (
|
||||
newState === this.stateObj!.state &&
|
||||
!this.stateObj!.attributes.assumed_state
|
||||
)
|
||||
return;
|
||||
const service = newState === "on" ? "turn_on" : "turn_off";
|
||||
const domain = computeDomain(this.stateObj!.entity_id);
|
||||
|
||||
try {
|
||||
await this.hass!.callService(domain, service, {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
});
|
||||
} catch (_err) {
|
||||
showToast(this, {
|
||||
message: this.hass!.localize("ui.notification_toast.action_failed", {
|
||||
service: domain + "." + service,
|
||||
}),
|
||||
duration: 5000,
|
||||
dismissable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static styles = cardFeatureStyles;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-toggle-card-feature": HuiToggleCardFeature;
|
||||
}
|
||||
}
|
||||
@@ -83,6 +83,15 @@ export interface ClimatePresetModesCardFeatureConfig {
|
||||
preset_modes?: string[];
|
||||
}
|
||||
|
||||
export const COUNTER_ACTIONS = ["increment", "reset", "decrement"] as const;
|
||||
|
||||
export type CounterActions = (typeof COUNTER_ACTIONS)[number];
|
||||
|
||||
export interface CounterActionsCardFeatureConfig {
|
||||
type: "counter-actions";
|
||||
actions?: CounterActions[];
|
||||
}
|
||||
|
||||
export interface SelectOptionsCardFeatureConfig {
|
||||
type: "select-options";
|
||||
options?: string[];
|
||||
@@ -101,6 +110,10 @@ export interface TargetTemperatureCardFeatureConfig {
|
||||
type: "target-temperature";
|
||||
}
|
||||
|
||||
export interface ToggleCardFeatureConfig {
|
||||
type: "toggle";
|
||||
}
|
||||
|
||||
export interface WaterHeaterOperationModesCardFeatureConfig {
|
||||
type: "water-heater-operation-modes";
|
||||
operation_modes?: OperationMode[];
|
||||
@@ -152,6 +165,7 @@ export type LovelaceCardFeatureConfig =
|
||||
| ClimateSwingHorizontalModesCardFeatureConfig
|
||||
| ClimateHvacModesCardFeatureConfig
|
||||
| ClimatePresetModesCardFeatureConfig
|
||||
| CounterActionsCardFeatureConfig
|
||||
| CoverOpenCloseCardFeatureConfig
|
||||
| CoverPositionCardFeatureConfig
|
||||
| CoverTiltPositionCardFeatureConfig
|
||||
@@ -170,6 +184,7 @@ export type LovelaceCardFeatureConfig =
|
||||
| SelectOptionsCardFeatureConfig
|
||||
| TargetHumidityCardFeatureConfig
|
||||
| TargetTemperatureCardFeatureConfig
|
||||
| ToggleCardFeatureConfig
|
||||
| UpdateActionsCardFeatureConfig
|
||||
| VacuumCommandsCardFeatureConfig
|
||||
| WaterHeaterOperationModesCardFeatureConfig;
|
||||
|
||||
@@ -291,20 +291,19 @@ export class HuiEnergyUsageGraphCard
|
||||
true
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// add empty dataset so compare bars are first
|
||||
// `stack: usage` so it doesn't take up space yet
|
||||
const firstId = statIds.from_grid?.[0] ?? "placeholder";
|
||||
datasets.push({
|
||||
id: "compare-" + firstId,
|
||||
type: "bar",
|
||||
stack: "usage",
|
||||
data: [],
|
||||
// @ts-expect-error
|
||||
order: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// add empty dataset so compare bars are first
|
||||
// `stack: usage` so it doesn't take up space yet
|
||||
datasets.push({
|
||||
id: "compare-placeholder",
|
||||
type: "bar",
|
||||
stack: energyData.statsCompare ? "compare" : "usage",
|
||||
data: [],
|
||||
// @ts-expect-error
|
||||
order: 0,
|
||||
});
|
||||
|
||||
datasets.push(
|
||||
...this._processDataSet(
|
||||
energyData.stats,
|
||||
|
||||
@@ -12,6 +12,7 @@ import memoizeOne from "memoize-one";
|
||||
import { getColorByIndex } from "../../../common/color/colors";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { deepEqual } from "../../../common/util/deep-equal";
|
||||
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
|
||||
@@ -80,14 +81,38 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
|
||||
private _subscribed?: Promise<(() => Promise<void>) | undefined>;
|
||||
|
||||
private _getAllEntities(): string[] {
|
||||
const hass = this.hass!;
|
||||
const personSources = new Set<string>();
|
||||
const locationEntities: string[] = [];
|
||||
Object.values(hass.states).forEach((entity) => {
|
||||
if (
|
||||
!("latitude" in entity.attributes) ||
|
||||
!("longitude" in entity.attributes)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
locationEntities.push(entity.entity_id);
|
||||
if (computeStateDomain(entity) === "person" && entity.attributes.source) {
|
||||
personSources.add(entity.attributes.source);
|
||||
}
|
||||
});
|
||||
|
||||
return locationEntities.filter((entity) => !personSources.has(entity));
|
||||
}
|
||||
|
||||
public setConfig(config: MapCardConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Error in card configuration.");
|
||||
}
|
||||
|
||||
if (!config.entities?.length && !config.geo_location_sources) {
|
||||
if (
|
||||
!config.show_all &&
|
||||
!config.entities?.length &&
|
||||
!config.geo_location_sources
|
||||
) {
|
||||
throw new Error(
|
||||
"Either entities or geo_location_sources must be specified"
|
||||
"Either show_all, entities, or geo_location_sources must be specified"
|
||||
);
|
||||
}
|
||||
if (config.entities && !Array.isArray(config.entities)) {
|
||||
@@ -99,10 +124,17 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
) {
|
||||
throw new Error("Parameter geo_location_sources needs to be an array");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
this._configEntities = config.entities
|
||||
? processConfigEntities<MapEntityConfig>(config.entities)
|
||||
if (config.show_all && (config.entities || config.geo_location_sources)) {
|
||||
throw new Error(
|
||||
"Cannot specify show_all and entities or geo_location_sources"
|
||||
);
|
||||
}
|
||||
this._config = { ...config };
|
||||
if (this.hass && config.show_all) {
|
||||
this._config.entities = this._getAllEntities();
|
||||
}
|
||||
this._configEntities = this._config.entities
|
||||
? processConfigEntities<MapEntityConfig>(this._config.entities)
|
||||
: [];
|
||||
this._mapEntities = this._getMapEntities();
|
||||
}
|
||||
@@ -239,6 +271,18 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (
|
||||
this._config?.show_all &&
|
||||
!this._config?.entities &&
|
||||
this.hass &&
|
||||
changedProps.has("hass")
|
||||
) {
|
||||
this._config.entities = this._getAllEntities();
|
||||
this._configEntities = processConfigEntities<MapEntityConfig>(
|
||||
this._config.entities
|
||||
);
|
||||
this._mapEntities = this._getMapEntities();
|
||||
}
|
||||
if (
|
||||
changedProps.has("hass") &&
|
||||
this._config?.geo_location_sources &&
|
||||
|
||||
@@ -107,18 +107,26 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
|
||||
return html`
|
||||
${this._error
|
||||
? html`<ha-alert
|
||||
alert-type=${this._errorLevel?.toLowerCase() || "error"}
|
||||
>${this._error}</ha-alert
|
||||
>`
|
||||
? html`
|
||||
<ha-alert
|
||||
.alertType=${(this._errorLevel?.toLowerCase() as
|
||||
| "error"
|
||||
| "warning") || "error"}
|
||||
>
|
||||
${this._error}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
<ha-card .header=${this._config.title}>
|
||||
<ha-card
|
||||
.header=${!this._config.text_only ? this._config.title : undefined}
|
||||
class=${classMap({
|
||||
"with-header": !!this._config.title,
|
||||
"text-only": this._config.text_only ?? false,
|
||||
})}
|
||||
>
|
||||
<ha-markdown
|
||||
cache
|
||||
breaks
|
||||
class=${classMap({
|
||||
"no-header": !this._config.title,
|
||||
})}
|
||||
.content=${this._templateResult?.result}
|
||||
></ha-markdown>
|
||||
</ha-card>
|
||||
@@ -228,11 +236,19 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
ha-markdown {
|
||||
padding: 0 16px 16px;
|
||||
padding: 16px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
ha-markdown.no-header {
|
||||
padding-top: 16px;
|
||||
.with-header ha-markdown {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
.text-only {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
.text-only ha-markdown {
|
||||
padding: 2px 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -416,15 +416,12 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.container.horizontal .content {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.vertical {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
@@ -458,9 +455,10 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
padding: 0 12px 12px 12px;
|
||||
}
|
||||
.container.horizontal hui-card-features {
|
||||
width: 50%;
|
||||
width: calc(50% - var(--column-gap, 0px) / 2 - 12px);
|
||||
flex: none;
|
||||
--feature-height: 36px;
|
||||
padding: 10px;
|
||||
padding: 0 12px;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -336,6 +336,7 @@ export interface MapCardConfig extends LovelaceCardConfig {
|
||||
export interface MarkdownCardConfig extends LovelaceCardConfig {
|
||||
type: "markdown";
|
||||
content: string;
|
||||
text_only?: boolean;
|
||||
title?: string;
|
||||
card_size?: number;
|
||||
entity_ids?: string | string[];
|
||||
|
||||
@@ -23,6 +23,7 @@ import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showEditBadgeDialog } from "../editor/badge-editor/show-edit-badge-dialog";
|
||||
import { addBadge } from "../editor/config-util";
|
||||
import type { LovelaceCardPath } from "../editor/lovelace-path";
|
||||
import {
|
||||
findLovelaceItems,
|
||||
@@ -221,11 +222,19 @@ export class HuiBadgeEditMode extends LitElement {
|
||||
const { cardIndex } = parseLovelaceCardPath(this.path!);
|
||||
const containerPath = getLovelaceContainerPath(this.path!);
|
||||
const badgeConfig = ensureBadgeConfig(this._badges![cardIndex]);
|
||||
|
||||
showEditBadgeDialog(this, {
|
||||
lovelaceConfig: this.lovelace!.config,
|
||||
saveConfig: this.lovelace!.saveConfig,
|
||||
path: containerPath,
|
||||
saveBadgeConfig: async (config) => {
|
||||
const newConfig = addBadge(
|
||||
this.lovelace!.config,
|
||||
containerPath,
|
||||
config
|
||||
);
|
||||
await this.lovelace!.saveConfig(newConfig);
|
||||
},
|
||||
badgeConfig,
|
||||
isNew: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,10 @@ import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||
import { addCard } from "../editor/config-util";
|
||||
import type { LovelaceCardPath } from "../editor/lovelace-path";
|
||||
import {
|
||||
findLovelaceContainer,
|
||||
findLovelaceItems,
|
||||
getLovelaceContainerPath,
|
||||
parseLovelaceCardPath,
|
||||
@@ -253,14 +255,24 @@ export class HuiCardEditMode extends LitElement {
|
||||
}
|
||||
|
||||
private _duplicateCard(): void {
|
||||
const { cardIndex } = parseLovelaceCardPath(this.path!);
|
||||
const { cardIndex, sectionIndex } = parseLovelaceCardPath(this.path!);
|
||||
const containerPath = getLovelaceContainerPath(this.path!);
|
||||
const sectionConfig =
|
||||
sectionIndex !== undefined
|
||||
? findLovelaceContainer(this.lovelace!.config, containerPath)
|
||||
: undefined;
|
||||
|
||||
const cardConfig = this._cards![cardIndex];
|
||||
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace!.config,
|
||||
saveConfig: this.lovelace!.saveConfig,
|
||||
path: containerPath,
|
||||
saveCardConfig: async (config) => {
|
||||
const newConfig = addCard(this.lovelace!.config, containerPath, config);
|
||||
await this.lovelace!.saveConfig(newConfig);
|
||||
},
|
||||
cardConfig,
|
||||
sectionConfig,
|
||||
isNew: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -278,9 +278,12 @@ export class HuiCardOptions extends LitElement {
|
||||
const cardConfig = this._cards![cardIndex];
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace!.config,
|
||||
saveConfig: this.lovelace!.saveConfig,
|
||||
path: containerPath,
|
||||
saveCardConfig: async (config) => {
|
||||
const newConfig = addCard(this.lovelace!.config, containerPath, config);
|
||||
await this.lovelace!.saveConfig(newConfig);
|
||||
},
|
||||
cardConfig,
|
||||
isNew: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import "../card-features/hui-climate-swing-modes-card-feature";
|
||||
import "../card-features/hui-climate-swing-horizontal-modes-card-feature";
|
||||
import "../card-features/hui-climate-hvac-modes-card-feature";
|
||||
import "../card-features/hui-climate-preset-modes-card-feature";
|
||||
import "../card-features/hui-counter-actions-card-feature";
|
||||
import "../card-features/hui-cover-open-close-card-feature";
|
||||
import "../card-features/hui-cover-position-card-feature";
|
||||
import "../card-features/hui-cover-tilt-card-feature";
|
||||
@@ -22,6 +23,7 @@ import "../card-features/hui-numeric-input-card-feature";
|
||||
import "../card-features/hui-select-options-card-feature";
|
||||
import "../card-features/hui-target-temperature-card-feature";
|
||||
import "../card-features/hui-target-humidity-card-feature";
|
||||
import "../card-features/hui-toggle-card-feature";
|
||||
import "../card-features/hui-update-actions-card-feature";
|
||||
import "../card-features/hui-vacuum-commands-card-feature";
|
||||
import "../card-features/hui-water-heater-operation-modes-card-feature";
|
||||
@@ -39,6 +41,7 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
|
||||
"climate-swing-horizontal-modes",
|
||||
"climate-hvac-modes",
|
||||
"climate-preset-modes",
|
||||
"counter-actions",
|
||||
"cover-open-close",
|
||||
"cover-position",
|
||||
"cover-tilt-position",
|
||||
@@ -57,6 +60,7 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
|
||||
"select-options",
|
||||
"target-humidity",
|
||||
"target-temperature",
|
||||
"toggle",
|
||||
"update-actions",
|
||||
"vacuum-commands",
|
||||
"water-heater-operation-modes",
|
||||
|
||||
@@ -24,6 +24,7 @@ import "./hui-badge-picker";
|
||||
import type { CreateBadgeDialogParams } from "./show-create-badge-dialog";
|
||||
import { showEditBadgeDialog } from "./show-edit-badge-dialog";
|
||||
import { showSuggestBadgeDialog } from "./show-suggest-badge-dialog";
|
||||
import { addBadge } from "../config-util";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -223,11 +224,22 @@ export class HuiCreateDialogBadge
|
||||
}
|
||||
}
|
||||
|
||||
const lovelaceConfig = this._params!.lovelaceConfig;
|
||||
const containerPath = this._params!.path;
|
||||
const saveConfig = this._params!.saveConfig;
|
||||
|
||||
showEditBadgeDialog(this, {
|
||||
lovelaceConfig: this._params!.lovelaceConfig,
|
||||
saveConfig: this._params!.saveConfig,
|
||||
path: this._params!.path,
|
||||
lovelaceConfig,
|
||||
saveBadgeConfig: async (newBadgeConfig) => {
|
||||
const newConfig = addBadge(
|
||||
lovelaceConfig,
|
||||
containerPath,
|
||||
newBadgeConfig
|
||||
);
|
||||
await saveConfig(newConfig);
|
||||
},
|
||||
badgeConfig: config,
|
||||
isNew: true,
|
||||
});
|
||||
|
||||
this.closeDialog();
|
||||
|
||||
@@ -11,8 +11,6 @@ import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
|
||||
import { ensureBadgeConfig } from "../../../../data/lovelace/config/badge";
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import {
|
||||
getCustomBadgeEntry,
|
||||
isCustomType,
|
||||
@@ -22,13 +20,12 @@ import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
import "../../badges/hui-badge";
|
||||
import "../../sections/hui-section";
|
||||
import { addBadge, replaceBadge } from "../config-util";
|
||||
import { getBadgeDocumentationURL } from "../get-dashboard-documentation-url";
|
||||
import type { ConfigChangedEvent } from "../hui-element-editor";
|
||||
import { findLovelaceContainer } from "../lovelace-path";
|
||||
import type { GUIModeChangedEvent } from "../types";
|
||||
import "./hui-badge-element-editor";
|
||||
import type { HuiBadgeElementEditor } from "./hui-badge-element-editor";
|
||||
@@ -58,8 +55,6 @@ export class HuiDialogEditBadge
|
||||
|
||||
@state() private _badgeConfig?: LovelaceBadgeConfig;
|
||||
|
||||
@state() private _containerConfig!: LovelaceViewConfig;
|
||||
|
||||
@state() private _saving = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
@@ -82,24 +77,7 @@ export class HuiDialogEditBadge
|
||||
this._GUImode = true;
|
||||
this._guiModeAvailable = true;
|
||||
|
||||
const containerConfig = findLovelaceContainer(
|
||||
params.lovelaceConfig,
|
||||
params.path
|
||||
);
|
||||
|
||||
if ("strategy" in containerConfig) {
|
||||
throw new Error("Can't edit strategy");
|
||||
}
|
||||
|
||||
this._containerConfig = containerConfig;
|
||||
|
||||
if ("badgeConfig" in params) {
|
||||
this._badgeConfig = params.badgeConfig;
|
||||
this._dirty = true;
|
||||
} else {
|
||||
const badge = this._containerConfig.badges?.[params.badgeIndex];
|
||||
this._badgeConfig = badge != null ? ensureBadgeConfig(badge) : badge;
|
||||
}
|
||||
this._badgeConfig = params.badgeConfig;
|
||||
|
||||
this.large = false;
|
||||
if (this._badgeConfig && !Object.isFrozen(this._badgeConfig)) {
|
||||
@@ -178,13 +156,6 @@ export class HuiDialogEditBadge
|
||||
"ui.panel.lovelace.editor.edit_badge.typed_header",
|
||||
{ type: badgeName }
|
||||
);
|
||||
} else if (!this._badgeConfig) {
|
||||
heading = this._containerConfig.title
|
||||
? this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_badge.pick_badge_view_title",
|
||||
{ name: this._containerConfig.title }
|
||||
)
|
||||
: this.hass!.localize("ui.panel.lovelace.editor.edit_badge.pick_badge");
|
||||
} else {
|
||||
heading = this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_badge.header"
|
||||
@@ -377,20 +348,18 @@ export class HuiDialogEditBadge
|
||||
return;
|
||||
}
|
||||
this._saving = true;
|
||||
const path = this._params!.path;
|
||||
await this._params!.saveConfig(
|
||||
"badgeConfig" in this._params!
|
||||
? addBadge(this._params!.lovelaceConfig, path, this._badgeConfig!)
|
||||
: replaceBadge(
|
||||
this._params!.lovelaceConfig,
|
||||
[...path, this._params!.badgeIndex],
|
||||
this._badgeConfig!
|
||||
)
|
||||
);
|
||||
this._saving = false;
|
||||
this._dirty = false;
|
||||
showSaveSuccessToast(this, this.hass);
|
||||
this.closeDialog();
|
||||
try {
|
||||
await this._params!.saveBadgeConfig(this._badgeConfig!);
|
||||
this._saving = false;
|
||||
this._dirty = false;
|
||||
showSaveSuccessToast(this, this.hass);
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
showToast(this, {
|
||||
message: err.message,
|
||||
});
|
||||
this._saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { LovelaceContainerPath } from "../lovelace-path";
|
||||
|
||||
export type EditBadgeDialogParams = {
|
||||
export interface EditBadgeDialogParams {
|
||||
lovelaceConfig: LovelaceConfig;
|
||||
saveConfig: (config: LovelaceConfig) => void;
|
||||
path: LovelaceContainerPath;
|
||||
} & (
|
||||
| {
|
||||
badgeIndex: number;
|
||||
}
|
||||
| {
|
||||
badgeConfig: LovelaceBadgeConfig;
|
||||
}
|
||||
);
|
||||
saveBadgeConfig: (badge: LovelaceBadgeConfig) => void;
|
||||
badgeConfig: LovelaceBadgeConfig;
|
||||
isNew?: boolean;
|
||||
}
|
||||
|
||||
export const importEditBadgeDialog = () => import("./hui-dialog-edit-badge");
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ import "@material/mwc-tab/mwc-tab";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoize from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
computeCards,
|
||||
computeSection,
|
||||
} from "../../common/generate-lovelace-config";
|
||||
import { addCard } from "../config-util";
|
||||
import {
|
||||
findLovelaceContainer,
|
||||
parseLovelaceContainerPath,
|
||||
@@ -241,11 +242,24 @@ export class HuiCreateDialogCard
|
||||
}
|
||||
}
|
||||
|
||||
const lovelaceConfig = this._params!.lovelaceConfig;
|
||||
const containerPath = this._params!.path;
|
||||
const saveConfig = this._params!.saveConfig;
|
||||
|
||||
const sectionConfig =
|
||||
containerPath.length === 2
|
||||
? findLovelaceContainer(lovelaceConfig, containerPath)
|
||||
: undefined;
|
||||
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this._params!.lovelaceConfig,
|
||||
saveConfig: this._params!.saveConfig,
|
||||
path: this._params!.path,
|
||||
lovelaceConfig,
|
||||
saveCardConfig: async (newCardConfig) => {
|
||||
const newConfig = addCard(lovelaceConfig, containerPath, newCardConfig);
|
||||
await saveConfig(newConfig);
|
||||
},
|
||||
cardConfig: config,
|
||||
sectionConfig,
|
||||
isNew: true,
|
||||
});
|
||||
|
||||
this.closeDialog();
|
||||
|
||||
@@ -13,7 +13,6 @@ import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import {
|
||||
getCustomCardEntry,
|
||||
isCustomType,
|
||||
@@ -23,13 +22,12 @@ import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
import "../../cards/hui-card";
|
||||
import "../../sections/hui-section";
|
||||
import { addCard, replaceCard } from "../config-util";
|
||||
import { getCardDocumentationURL } from "../get-dashboard-documentation-url";
|
||||
import type { ConfigChangedEvent } from "../hui-element-editor";
|
||||
import { findLovelaceContainer } from "../lovelace-path";
|
||||
import type { GUIModeChangedEvent } from "../types";
|
||||
import "./hui-card-element-editor";
|
||||
import type { HuiCardElementEditor } from "./hui-card-element-editor";
|
||||
@@ -59,9 +57,7 @@ export class HuiDialogEditCard
|
||||
|
||||
@state() private _cardConfig?: LovelaceCardConfig;
|
||||
|
||||
@state() private _containerConfig!:
|
||||
| LovelaceViewConfig
|
||||
| LovelaceSectionConfig;
|
||||
@state() private _sectionConfig?: LovelaceSectionConfig;
|
||||
|
||||
@state() private _saving = false;
|
||||
|
||||
@@ -85,23 +81,10 @@ export class HuiDialogEditCard
|
||||
this._GUImode = true;
|
||||
this._guiModeAvailable = true;
|
||||
|
||||
const containerConfig = findLovelaceContainer(
|
||||
params.lovelaceConfig,
|
||||
params.path
|
||||
);
|
||||
this._sectionConfig = this._params.sectionConfig;
|
||||
|
||||
if ("strategy" in containerConfig) {
|
||||
throw new Error("Can't edit strategy");
|
||||
}
|
||||
|
||||
this._containerConfig = containerConfig;
|
||||
|
||||
if ("cardConfig" in params) {
|
||||
this._cardConfig = params.cardConfig;
|
||||
this._dirty = true;
|
||||
} else {
|
||||
this._cardConfig = this._containerConfig.cards?.[params.cardIndex];
|
||||
}
|
||||
this._cardConfig = params.cardConfig;
|
||||
this._dirty = Boolean(this._params.isNew);
|
||||
|
||||
this.large = false;
|
||||
if (this._cardConfig && !Object.isFrozen(this._cardConfig)) {
|
||||
@@ -156,12 +139,12 @@ export class HuiDialogEditCard
|
||||
};
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
if (!this._params || !this._cardConfig) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
let heading: string;
|
||||
if (this._cardConfig && this._cardConfig.type) {
|
||||
if (this._cardConfig.type) {
|
||||
let cardName: string | undefined;
|
||||
if (isCustomType(this._cardConfig.type)) {
|
||||
// prettier-ignore
|
||||
@@ -181,13 +164,6 @@ export class HuiDialogEditCard
|
||||
"ui.panel.lovelace.editor.edit_card.typed_header",
|
||||
{ type: cardName }
|
||||
);
|
||||
} else if (!this._cardConfig) {
|
||||
heading = this._containerConfig.title
|
||||
? this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.pick_card_view_title",
|
||||
{ name: this._containerConfig.title }
|
||||
)
|
||||
: this.hass!.localize("ui.panel.lovelace.editor.edit_card.pick_card");
|
||||
} else {
|
||||
heading = this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.header"
|
||||
@@ -230,10 +206,8 @@ export class HuiDialogEditCard
|
||||
<div class="content">
|
||||
<div class="element-editor">
|
||||
<hui-card-element-editor
|
||||
.showVisibilityTab=${this._cardConfig?.type !== "conditional"}
|
||||
.sectionConfig=${this._isInSection
|
||||
? this._containerConfig
|
||||
: undefined}
|
||||
.showVisibilityTab=${this._cardConfig.type !== "conditional"}
|
||||
.sectionConfig=${this._sectionConfig}
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this._params.lovelaceConfig}
|
||||
.value=${this._cardConfig}
|
||||
@@ -244,7 +218,7 @@ export class HuiDialogEditCard
|
||||
></hui-card-element-editor>
|
||||
</div>
|
||||
<div class="element-preview">
|
||||
${this._isInSection
|
||||
${this._sectionConfig
|
||||
? html`
|
||||
<hui-section
|
||||
.hass=${this.hass}
|
||||
@@ -345,14 +319,10 @@ export class HuiDialogEditCard
|
||||
this._cardEditorEl?.focusYamlEditor();
|
||||
}
|
||||
|
||||
private get _isInSection() {
|
||||
return this._params!.path.length === 2;
|
||||
}
|
||||
|
||||
private _cardConfigInSection = memoizeOne(
|
||||
(cardConfig?: LovelaceCardConfig) => {
|
||||
(cardConfig: LovelaceCardConfig) => {
|
||||
const { cards, title, ...containerConfig } = this
|
||||
._containerConfig as LovelaceSectionConfig;
|
||||
._sectionConfig as LovelaceSectionConfig;
|
||||
|
||||
return {
|
||||
...containerConfig,
|
||||
@@ -411,20 +381,18 @@ export class HuiDialogEditCard
|
||||
return;
|
||||
}
|
||||
this._saving = true;
|
||||
const path = this._params!.path;
|
||||
await this._params!.saveConfig(
|
||||
"cardConfig" in this._params!
|
||||
? addCard(this._params!.lovelaceConfig, path, this._cardConfig!)
|
||||
: replaceCard(
|
||||
this._params!.lovelaceConfig,
|
||||
[...path, this._params!.cardIndex],
|
||||
this._cardConfig!
|
||||
)
|
||||
);
|
||||
this._saving = false;
|
||||
this._dirty = false;
|
||||
showSaveSuccessToast(this, this.hass);
|
||||
this.closeDialog();
|
||||
try {
|
||||
await this._params!.saveCardConfig(this._cardConfig!);
|
||||
this._saving = false;
|
||||
this._dirty = false;
|
||||
showSaveSuccessToast(this, this.hass);
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
showToast(this, {
|
||||
message: err.message,
|
||||
});
|
||||
this._saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { LovelaceContainerPath } from "../lovelace-path";
|
||||
|
||||
export type EditCardDialogParams = {
|
||||
export interface EditCardDialogParams {
|
||||
lovelaceConfig: LovelaceConfig;
|
||||
saveConfig: (config: LovelaceConfig) => void;
|
||||
path: LovelaceContainerPath;
|
||||
} & (
|
||||
| {
|
||||
cardIndex: number;
|
||||
}
|
||||
| {
|
||||
cardConfig: LovelaceCardConfig;
|
||||
}
|
||||
);
|
||||
saveCardConfig: (config: LovelaceCardConfig) => void;
|
||||
cardConfig: LovelaceCardConfig;
|
||||
sectionConfig?: LovelaceSectionConfig;
|
||||
isNew?: boolean;
|
||||
}
|
||||
|
||||
export const importEditCardDialog = () => import("./hui-dialog-edit-card");
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import { supportsClimateHvacModesCardFeature } from "../../card-features/hui-cli
|
||||
import { supportsClimatePresetModesCardFeature } from "../../card-features/hui-climate-preset-modes-card-feature";
|
||||
import { supportsClimateSwingModesCardFeature } from "../../card-features/hui-climate-swing-modes-card-feature";
|
||||
import { supportsClimateSwingHorizontalModesCardFeature } from "../../card-features/hui-climate-swing-horizontal-modes-card-feature";
|
||||
import { supportsCounterActionsCardFeature } from "../../card-features/hui-counter-actions-card-feature";
|
||||
import { supportsCoverOpenCloseCardFeature } from "../../card-features/hui-cover-open-close-card-feature";
|
||||
import { supportsCoverPositionCardFeature } from "../../card-features/hui-cover-position-card-feature";
|
||||
import { supportsCoverTiltCardFeature } from "../../card-features/hui-cover-tilt-card-feature";
|
||||
@@ -42,6 +43,7 @@ import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric
|
||||
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
|
||||
import { supportsTargetHumidityCardFeature } from "../../card-features/hui-target-humidity-card-feature";
|
||||
import { supportsTargetTemperatureCardFeature } from "../../card-features/hui-target-temperature-card-feature";
|
||||
import { supportsToggleCardFeature } from "../../card-features/hui-toggle-card-feature";
|
||||
import { supportsUpdateActionsCardFeature } from "../../card-features/hui-update-actions-card-feature";
|
||||
import { supportsVacuumCommandsCardFeature } from "../../card-features/hui-vacuum-commands-card-feature";
|
||||
import { supportsWaterHeaterOperationModesCardFeature } from "../../card-features/hui-water-heater-operation-modes-card-feature";
|
||||
@@ -58,6 +60,7 @@ const UI_FEATURE_TYPES = [
|
||||
"climate-preset-modes",
|
||||
"climate-swing-modes",
|
||||
"climate-swing-horizontal-modes",
|
||||
"counter-actions",
|
||||
"cover-open-close",
|
||||
"cover-position",
|
||||
"cover-tilt-position",
|
||||
@@ -76,6 +79,7 @@ const UI_FEATURE_TYPES = [
|
||||
"select-options",
|
||||
"target-humidity",
|
||||
"target-temperature",
|
||||
"toggle",
|
||||
"update-actions",
|
||||
"vacuum-commands",
|
||||
"water-heater-operation-modes",
|
||||
@@ -90,6 +94,7 @@ const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
|
||||
"climate-preset-modes",
|
||||
"climate-swing-modes",
|
||||
"climate-swing-horizontal-modes",
|
||||
"counter-actions",
|
||||
"fan-preset-modes",
|
||||
"humidifier-modes",
|
||||
"lawn-mower-commands",
|
||||
@@ -111,6 +116,7 @@ const SUPPORTS_FEATURE_TYPES: Record<
|
||||
supportsClimateSwingHorizontalModesCardFeature,
|
||||
"climate-hvac-modes": supportsClimateHvacModesCardFeature,
|
||||
"climate-preset-modes": supportsClimatePresetModesCardFeature,
|
||||
"counter-actions": supportsCounterActionsCardFeature,
|
||||
"cover-open-close": supportsCoverOpenCloseCardFeature,
|
||||
"cover-position": supportsCoverPositionCardFeature,
|
||||
"cover-tilt-position": supportsCoverTiltPositionCardFeature,
|
||||
@@ -129,6 +135,7 @@ const SUPPORTS_FEATURE_TYPES: Record<
|
||||
"select-options": supportsSelectOptionsCardFeature,
|
||||
"target-humidity": supportsTargetHumidityCardFeature,
|
||||
"target-temperature": supportsTargetTemperatureCardFeature,
|
||||
toggle: supportsToggleCardFeature,
|
||||
"update-actions": supportsUpdateActionsCardFeature,
|
||||
"vacuum-commands": supportsVacuumCommandsCardFeature,
|
||||
"water-heater-operation-modes": supportsWaterHeaterOperationModesCardFeature,
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
COUNTER_ACTIONS,
|
||||
type LovelaceCardFeatureContext,
|
||||
type CounterActionsCardFeatureConfig,
|
||||
} from "../../card-features/types";
|
||||
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||
|
||||
@customElement("hui-counter-actions-card-feature-editor")
|
||||
export class HuiCounterActionsCardFeatureEditor
|
||||
extends LitElement
|
||||
implements LovelaceCardFeatureEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||
|
||||
@state() private _config?: CounterActionsCardFeatureConfig;
|
||||
|
||||
public setConfig(config: CounterActionsCardFeatureConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
{
|
||||
name: "actions",
|
||||
selector: {
|
||||
select: {
|
||||
multiple: true,
|
||||
mode: "list",
|
||||
reorder: true,
|
||||
options: COUNTER_ACTIONS.map((action) => ({
|
||||
value: action,
|
||||
label: `${localize(
|
||||
`ui.panel.lovelace.editor.features.types.counter-actions.actions.${action}`
|
||||
)}`,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const schema = this._schema(this.hass.localize);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-counter-actions-card-feature-editor": HuiCounterActionsCardFeatureEditor;
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ const cardConfigStruct = assign(
|
||||
aspect_ratio: optional(string()),
|
||||
default_zoom: optional(number()),
|
||||
dark_mode: optional(boolean()),
|
||||
entities: array(mapEntitiesConfigStruct),
|
||||
entities: optional(array(mapEntitiesConfigStruct)),
|
||||
hours_to_show: optional(number()),
|
||||
geo_location_sources: optional(array(geoSourcesConfigStruct)),
|
||||
auto_fit: optional(boolean()),
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { assert, assign, object, optional, string } from "superstruct";
|
||||
import { assert, assign, boolean, object, optional, string } from "superstruct";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { MarkdownCardConfig } from "../../cards/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
object({
|
||||
text_only: optional(boolean()),
|
||||
title: optional(string()),
|
||||
content: string(),
|
||||
theme: optional(string()),
|
||||
})
|
||||
);
|
||||
|
||||
const SCHEMA = [
|
||||
{ name: "title", selector: { text: {} } },
|
||||
{ name: "content", required: true, selector: { template: {} } },
|
||||
{ name: "theme", selector: { theme: {} } },
|
||||
] as const;
|
||||
|
||||
@customElement("hui-markdown-card-editor")
|
||||
export class HuiMarkdownCardEditor
|
||||
extends LitElement
|
||||
@@ -38,16 +37,53 @@ export class HuiMarkdownCardEditor
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, text_only: boolean, isDark: boolean) =>
|
||||
[
|
||||
{
|
||||
name: "style",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "box",
|
||||
options: ["card", "text-only"].map((style) => ({
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.markdown.style_options.${style}`
|
||||
),
|
||||
image: `/static/images/form/markdown_${style.replace("-", "_")}${isDark ? "_dark" : ""}.svg`,
|
||||
value: style,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
...(!text_only
|
||||
? ([{ name: "title", selector: { text: {} } }] as const)
|
||||
: []),
|
||||
{ name: "content", required: true, selector: { template: {} } },
|
||||
] as const satisfies HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const data = {
|
||||
...this._config,
|
||||
style: this._config.text_only ? "text-only" : "card",
|
||||
};
|
||||
|
||||
const schema = this._schema(
|
||||
this.hass.localize,
|
||||
this._config.text_only || false,
|
||||
this.hass.themes.darkMode
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.schema=${SCHEMA}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
@@ -55,17 +91,23 @@ export class HuiMarkdownCardEditor
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
const config = { ...ev.detail.value };
|
||||
|
||||
if (config.style === "text-only") {
|
||||
config.text_only = true;
|
||||
} else {
|
||||
delete config.text_only;
|
||||
}
|
||||
delete config.style;
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "theme":
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
case "style":
|
||||
case "content":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.markdown.${schema.name}`
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
optional,
|
||||
string,
|
||||
} from "superstruct";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
@@ -84,6 +85,8 @@ export class HuiStackCardEditor
|
||||
|
||||
@state() protected _guiModeAvailable? = true;
|
||||
|
||||
protected _keys = new WeakMap<LovelaceCardConfig, string>();
|
||||
|
||||
protected _schema: readonly HaFormSchema[] = SCHEMA;
|
||||
|
||||
@query("hui-card-element-editor")
|
||||
@@ -199,14 +202,16 @@ export class HuiStackCardEditor
|
||||
@click=${this._handleDeleteCard}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
|
||||
<hui-card-element-editor
|
||||
.hass=${this.hass}
|
||||
.value=${this._config.cards[selected]}
|
||||
.lovelace=${this.lovelace}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
></hui-card-element-editor>
|
||||
${keyed(
|
||||
this._getKey(this._config.cards[selected]),
|
||||
html`<hui-card-element-editor
|
||||
.hass=${this.hass}
|
||||
.value=${this._config.cards[selected]}
|
||||
.lovelace=${this.lovelace}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
></hui-card-element-editor>`
|
||||
)}
|
||||
`
|
||||
: html`
|
||||
<hui-card-picker
|
||||
@@ -220,6 +225,14 @@ export class HuiStackCardEditor
|
||||
`;
|
||||
}
|
||||
|
||||
private _getKey(card: LovelaceCardConfig) {
|
||||
if (!this._keys.has(card)) {
|
||||
this._keys.set(card, Math.random().toString());
|
||||
}
|
||||
|
||||
return this._keys.get(card)!;
|
||||
}
|
||||
|
||||
protected _handleSelectedCard(ev) {
|
||||
if (ev.target.id === "add-card") {
|
||||
this._selectedCard = this._config!.cards.length;
|
||||
@@ -236,7 +249,10 @@ export class HuiStackCardEditor
|
||||
return;
|
||||
}
|
||||
const cards = [...this._config.cards];
|
||||
cards[this._selectedCard] = ev.detail.config as LovelaceCardConfig;
|
||||
const key = this._getKey(cards[this._selectedCard]);
|
||||
const newCard = ev.detail.config as LovelaceCardConfig;
|
||||
cards[this._selectedCard] = newCard;
|
||||
this._keys.set(newCard, key);
|
||||
this._config = { ...this._config, cards };
|
||||
this._guiModeAvailable = ev.detail.guiModeAvailable;
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
|
||||
@@ -115,7 +115,7 @@ export class HuiTileCardEditor
|
||||
localize: LocalizeFunc,
|
||||
entityId: string | undefined,
|
||||
hideState: boolean,
|
||||
vertical: boolean,
|
||||
isDark: boolean,
|
||||
displayActions: AdvancedActions[] = []
|
||||
) =>
|
||||
[
|
||||
@@ -175,41 +175,20 @@ export class HuiTileCardEditor
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "content_layout",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: ["horizontal", "vertical"].map((value) => ({
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.tile.content_layout_options.${value}`
|
||||
),
|
||||
value,
|
||||
})),
|
||||
},
|
||||
},
|
||||
name: "content_layout",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "box",
|
||||
options: ["horizontal", "vertical"].map((value) => ({
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.tile.content_layout_options.${value}`
|
||||
),
|
||||
value,
|
||||
image: `/static/images/form/tile_content_layout_${value}${isDark ? "_dark" : ""}.svg`,
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: "features_position",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: ["bottom", "inline"].map((value) => ({
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.tile.features_position_options.${value}`
|
||||
),
|
||||
value,
|
||||
disabled: vertical && value === "inline",
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -250,6 +229,29 @@ export class HuiTileCardEditor
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
private _featuresSchema = memoizeOne(
|
||||
(localize: LocalizeFunc, vertical: boolean, isDark: boolean) =>
|
||||
[
|
||||
{
|
||||
name: "features_position",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "box",
|
||||
options: ["bottom", "inline"].map((value) => ({
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.tile.features_position_options.${value}`
|
||||
),
|
||||
value,
|
||||
image: `/static/images/form/tile_features_position_${value}${isDark ? "_dark" : ""}.svg`,
|
||||
disabled: vertical && value === "inline",
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
@@ -262,10 +264,16 @@ export class HuiTileCardEditor
|
||||
this.hass.localize,
|
||||
entityId,
|
||||
this._config.hide_state ?? false,
|
||||
this._config.vertical ?? false,
|
||||
this.hass.themes.darkMode,
|
||||
this._displayActions
|
||||
);
|
||||
|
||||
const featuresSchema = this._featuresSchema(
|
||||
this.hass.localize,
|
||||
this._config.vertical ?? false,
|
||||
this.hass.themes.darkMode
|
||||
);
|
||||
|
||||
const data = {
|
||||
...this._config,
|
||||
content_layout: this._config.vertical ? "vertical" : "horizontal",
|
||||
@@ -293,6 +301,15 @@ export class HuiTileCardEditor
|
||||
)}
|
||||
</h3>
|
||||
<div class="content">
|
||||
<ha-form
|
||||
class="features-form"
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${featuresSchema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.computeHelper=${this._computeHelperCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
<hui-card-features-editor
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
@@ -381,7 +398,9 @@ export class HuiTileCardEditor
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
schema:
|
||||
| SchemaUnion<ReturnType<typeof this._schema>>
|
||||
| SchemaUnion<ReturnType<typeof this._featuresSchema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "color":
|
||||
@@ -405,13 +424,22 @@ export class HuiTileCardEditor
|
||||
};
|
||||
|
||||
private _computeHelperCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
schema:
|
||||
| SchemaUnion<ReturnType<typeof this._schema>>
|
||||
| SchemaUnion<ReturnType<typeof this._featuresSchema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "color":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.tile.${schema.name}_helper`
|
||||
);
|
||||
case "features_position":
|
||||
if (this._config?.vertical) {
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.tile.${schema.name}_helper_vertical`
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { createSectionElement } from "../create-element/create-section-element";
|
||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||
import { replaceCard } from "../editor/config-util";
|
||||
import { performDeleteCard } from "../editor/delete-card";
|
||||
import { parseLovelaceCardPath } from "../editor/lovelace-path";
|
||||
import { generateLovelaceSectionStrategy } from "../strategies/get-strategy";
|
||||
@@ -253,11 +254,23 @@ export class HuiSection extends ReactiveElement {
|
||||
ev.stopPropagation();
|
||||
if (!this.lovelace) return;
|
||||
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
|
||||
const sectionConfig = this.config;
|
||||
if (isStrategySection(sectionConfig)) {
|
||||
return;
|
||||
}
|
||||
const cardConfig = sectionConfig.cards![cardIndex];
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: [this.viewIndex, this.index],
|
||||
cardIndex,
|
||||
saveCardConfig: async (newCardConfig) => {
|
||||
const newConfig = replaceCard(
|
||||
this.lovelace!.config,
|
||||
[this.viewIndex, this.index, cardIndex],
|
||||
newCardConfig
|
||||
);
|
||||
await this.lovelace!.saveConfig(newConfig);
|
||||
},
|
||||
sectionConfig,
|
||||
cardConfig,
|
||||
});
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-delete-card", (ev) => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { computeStateDomain } from "../../../../common/entity/compute_state_domain";
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { MapCardConfig } from "../../cards/types";
|
||||
@@ -9,32 +8,12 @@ export interface MapViewStrategyConfig {
|
||||
type: "map";
|
||||
}
|
||||
|
||||
const getMapEntities = (hass: HomeAssistant) => {
|
||||
const personSources = new Set<string>();
|
||||
const locationEntities: string[] = [];
|
||||
Object.values(hass.states).forEach((entity) => {
|
||||
if (
|
||||
!("latitude" in entity.attributes) ||
|
||||
!("longitude" in entity.attributes)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
locationEntities.push(entity.entity_id);
|
||||
if (computeStateDomain(entity) === "person" && entity.attributes.source) {
|
||||
personSources.add(entity.attributes.source);
|
||||
}
|
||||
});
|
||||
|
||||
return locationEntities.filter((entity) => !personSources.has(entity));
|
||||
};
|
||||
|
||||
@customElement("map-view-strategy")
|
||||
export class MapViewStrategy extends ReactiveElement {
|
||||
static async generate(
|
||||
_config: MapViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const entities = getMapEntities(hass);
|
||||
return {
|
||||
type: "panel",
|
||||
title: hass.localize("panel.map"),
|
||||
@@ -43,7 +22,7 @@ export class MapViewStrategy extends ReactiveElement {
|
||||
{
|
||||
type: "map",
|
||||
auto_fit: true,
|
||||
entities: entities,
|
||||
show_all: true,
|
||||
} as MapCardConfig,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ import { showCreateBadgeDialog } from "../editor/badge-editor/show-create-badge-
|
||||
import { showEditBadgeDialog } from "../editor/badge-editor/show-edit-badge-dialog";
|
||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||
import { replaceCard } from "../editor/config-util";
|
||||
import {
|
||||
type DeleteBadgeParams,
|
||||
performDeleteBadge,
|
||||
@@ -270,11 +271,22 @@ export class HUIView extends ReactiveElement {
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-edit-card", (ev) => {
|
||||
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
|
||||
const viewConfig = this.lovelace!.config.views[this.index];
|
||||
if (isStrategyView(viewConfig)) {
|
||||
return;
|
||||
}
|
||||
const cardConfig = viewConfig.cards![cardIndex];
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: [this.index],
|
||||
cardIndex,
|
||||
saveCardConfig: async (newCardConfig) => {
|
||||
const newConfig = replaceCard(
|
||||
this.lovelace!.config,
|
||||
[this.index, cardIndex],
|
||||
newCardConfig
|
||||
);
|
||||
await this.lovelace.saveConfig(newConfig);
|
||||
},
|
||||
cardConfig,
|
||||
});
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-delete-card", (ev) => {
|
||||
@@ -290,11 +302,23 @@ export class HUIView extends ReactiveElement {
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-edit-badge", (ev) => {
|
||||
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
|
||||
const viewConfig = this.lovelace!.config.views[this.index];
|
||||
if (isStrategyView(viewConfig)) {
|
||||
return;
|
||||
}
|
||||
const badgeConfig = ensureBadgeConfig(viewConfig.badges![cardIndex]);
|
||||
|
||||
showEditBadgeDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: [this.index],
|
||||
badgeIndex: cardIndex,
|
||||
saveBadgeConfig: async (newBadgeConfig) => {
|
||||
const newConfig = replaceCard(
|
||||
this.lovelace!.config,
|
||||
[this.index, cardIndex],
|
||||
newBadgeConfig
|
||||
);
|
||||
await this.lovelace.saveConfig(newConfig);
|
||||
},
|
||||
badgeConfig,
|
||||
});
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-delete-badge", async (ev) => {
|
||||
|
||||
@@ -47,7 +47,12 @@ export class HaStateControlAlarmControlPanelModes extends LitElement {
|
||||
}
|
||||
|
||||
private async _setMode(mode: AlarmMode) {
|
||||
setProtectedAlarmControlPanelMode(this, this.hass!, this.stateObj!, mode);
|
||||
await setProtectedAlarmControlPanelMode(
|
||||
this,
|
||||
this.hass!,
|
||||
this.stateObj!,
|
||||
mode
|
||||
);
|
||||
}
|
||||
|
||||
private async _valueChanged(ev: CustomEvent) {
|
||||
|
||||
@@ -286,7 +286,10 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
clearInterval(this.__backendPingInterval);
|
||||
this.__backendPingInterval = setInterval(() => {
|
||||
if (this.hass?.connected) {
|
||||
promiseTimeout(5000, this.hass?.connection.ping()).catch(() => {
|
||||
// If the backend is busy, or the connection is latent,
|
||||
// it can take more than 10 seconds for the ping to return.
|
||||
// We give it a 15 second timeout to be safe.
|
||||
promiseTimeout(15000, this.hass?.connection.ping()).catch(() => {
|
||||
if (!this.hass?.connected) {
|
||||
return;
|
||||
}
|
||||
@@ -296,7 +299,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
this.hass?.connection.reconnect(true);
|
||||
});
|
||||
}
|
||||
}, 10000);
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
protected hassReconnected() {
|
||||
|
||||
@@ -370,7 +370,10 @@
|
||||
"name": "Name",
|
||||
"optional": "optional",
|
||||
"default": "Default",
|
||||
"dont_save": "Don't save"
|
||||
"dont_save": "Don't save",
|
||||
"copy": "Copy",
|
||||
"show": "Show",
|
||||
"hide": "Hide"
|
||||
},
|
||||
"components": {
|
||||
"selectors": {
|
||||
@@ -4727,6 +4730,23 @@
|
||||
"fingerprint": "Certificate fingerprint:",
|
||||
"close": "Close"
|
||||
},
|
||||
"dialog_already_connected": {
|
||||
"heading": "Account linked to other Home Assistant",
|
||||
"description": "We noticed that another instance is currently connected to your Home Assistant Cloud account. Your Home Assistant Cloud account can only be signed into one Home Assistant instance at a time. If you log in here, the other instance will be disconnected along with its Cloud services.",
|
||||
"other_home_assistant": "Other Home Assistant",
|
||||
"ip_address": "IP Address",
|
||||
"connected_at": "Connected at",
|
||||
"obfuscated_ip": {
|
||||
"show": "Show IP address",
|
||||
"hide": "Hide IP address"
|
||||
},
|
||||
"info_backups": {
|
||||
"title": "Home Assistant Cloud backups",
|
||||
"description": "Your Cloud backup may be overwritten if you proceed. We strongly recommend downloading your current backup from your Nabu Casa account page before continuing."
|
||||
},
|
||||
"close": "Close",
|
||||
"login_here": "Log in here"
|
||||
},
|
||||
"dialog_cloudhook": {
|
||||
"webhook_for": "Webhook for {name}",
|
||||
"managed_by_integration": "This webhook is managed by an integration and cannot be disabled.",
|
||||
@@ -5349,6 +5369,7 @@
|
||||
"source": "Source",
|
||||
"rssi": "RSSI",
|
||||
"source_address": "Source address",
|
||||
"last_seen": "Last seen",
|
||||
"device": "Device",
|
||||
"device_information": "Device information",
|
||||
"advertisement_data": "Advertisement data",
|
||||
@@ -6998,7 +7019,8 @@
|
||||
"suggested_cards": "Suggested cards",
|
||||
"other_cards": "Other cards",
|
||||
"custom_cards": "Custom cards",
|
||||
"features": "Features"
|
||||
"features": "Features",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"heading": {
|
||||
"name": "Heading",
|
||||
@@ -7045,6 +7067,11 @@
|
||||
"markdown": {
|
||||
"name": "Markdown",
|
||||
"content": "Content",
|
||||
"style": "Style",
|
||||
"style_options": {
|
||||
"card": "Card",
|
||||
"text-only": "Text only"
|
||||
},
|
||||
"description": "The Markdown card is used to render Markdown."
|
||||
},
|
||||
"media-control": {
|
||||
@@ -7132,6 +7159,7 @@
|
||||
"bottom": "Bottom",
|
||||
"inline": "Inline"
|
||||
},
|
||||
"features_position_helper_vertical": "Always displayed at the bottom if the content layout is vertical",
|
||||
"content_layout": "Content layout",
|
||||
"content_layout_options": {
|
||||
"horizontal": "Horizontal",
|
||||
@@ -7312,6 +7340,14 @@
|
||||
"customize_modes": "Customize preset modes",
|
||||
"preset_modes": "Preset modes"
|
||||
},
|
||||
"counter-actions": {
|
||||
"label": "Counter actions",
|
||||
"actions": {
|
||||
"increment": "Increment",
|
||||
"decrement": "Decrement",
|
||||
"reset": "Reset"
|
||||
}
|
||||
},
|
||||
"fan-preset-modes": {
|
||||
"label": "Fan preset modes",
|
||||
"style": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style%]",
|
||||
@@ -7340,6 +7376,9 @@
|
||||
"options": "Options",
|
||||
"customize_options": "Customize options"
|
||||
},
|
||||
"toggle": {
|
||||
"label": "Toggle"
|
||||
},
|
||||
"numeric-input": {
|
||||
"label": "Numeric input",
|
||||
"style": "Style",
|
||||
|
||||
@@ -63,4 +63,28 @@ describe("canToggleState", () => {
|
||||
};
|
||||
assert.isFalse(canToggleState(hass, stateObj));
|
||||
});
|
||||
|
||||
it("Detects group with missing entity", () => {
|
||||
const stateObj: any = {
|
||||
entity_id: "group.bla",
|
||||
state: "on",
|
||||
attributes: {
|
||||
entity_id: ["light.non_existing"],
|
||||
},
|
||||
};
|
||||
|
||||
assert.isFalse(canToggleState(hass, stateObj));
|
||||
});
|
||||
|
||||
it("Detects group with off state", () => {
|
||||
const stateObj: any = {
|
||||
entity_id: "group.bla",
|
||||
state: "off",
|
||||
attributes: {
|
||||
entity_id: ["light.test"],
|
||||
},
|
||||
};
|
||||
|
||||
assert.isTrue(canToggleState(hass, stateObj));
|
||||
});
|
||||
});
|
||||
|
||||
371
test/common/entity/compute_attribute_display.test.ts
Normal file
@@ -0,0 +1,371 @@
|
||||
import type {
|
||||
HassConfig,
|
||||
HassEntity,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
computeAttributeValueDisplay,
|
||||
computeAttributeNameDisplay,
|
||||
} from "../../../src/common/entity/compute_attribute_display";
|
||||
import type { FrontendLocaleData } from "../../../src/data/translation";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
|
||||
export const localizeMock = (key: string) => {
|
||||
const translations = {
|
||||
"state.default.unknown": "Unknown",
|
||||
"component.test_platform.entity.sensor.test_translation_key.state_attributes.attribute.state.42":
|
||||
"42",
|
||||
"component.test_platform.entity.sensor.test_translation_key.state_attributes.attribute.state.attributeValue":
|
||||
"Localized Attribute Name",
|
||||
"component.media_player.entity_component.media_player.state_attributes.attribute.state.attributeValue":
|
||||
"Localized Media Player Attribute Name",
|
||||
"component.media_player.entity_component._.state_attributes.attribute.state.attributeValue":
|
||||
"Media Player Attribute Name",
|
||||
};
|
||||
return translations[key] || "";
|
||||
};
|
||||
|
||||
export const stateObjMock = {
|
||||
entity_id: "sensor.test",
|
||||
attributes: {
|
||||
device_class: "temperature",
|
||||
},
|
||||
} as HassEntityBase;
|
||||
|
||||
export const localeMock = {
|
||||
language: "en",
|
||||
} as FrontendLocaleData;
|
||||
|
||||
export const configMock = {
|
||||
unit_system: {
|
||||
temperature: "°C",
|
||||
},
|
||||
} as HassConfig;
|
||||
|
||||
export const entitiesMock = {
|
||||
"sensor.test": {
|
||||
platform: "test_platform",
|
||||
translation_key: "test_translation_key",
|
||||
},
|
||||
"media_player.test": {
|
||||
platform: "media_player",
|
||||
},
|
||||
} as unknown as HomeAssistant["entities"];
|
||||
|
||||
describe("computeAttributeValueDisplay", () => {
|
||||
it("should return unknown state for null value", () => {
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObjMock,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"attribute",
|
||||
null
|
||||
);
|
||||
expect(result).toBe("Unknown");
|
||||
});
|
||||
|
||||
it("should return formatted number for numeric value", () => {
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObjMock,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"attribute",
|
||||
42
|
||||
);
|
||||
expect(result).toBe("42");
|
||||
});
|
||||
|
||||
it("should return number from formatter", () => {
|
||||
const stateObj = {
|
||||
entity_id: "media_player.test",
|
||||
attributes: {
|
||||
device_class: "media_player",
|
||||
volume_level: 0.42,
|
||||
},
|
||||
} as unknown as HassEntityBase;
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObj,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"volume_level"
|
||||
);
|
||||
expect(result).toBe("42%");
|
||||
});
|
||||
|
||||
it("should return formatted date for date string", () => {
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObjMock,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"attribute",
|
||||
"2023-10-10"
|
||||
);
|
||||
expect(result).toBe("October 10, 2023");
|
||||
});
|
||||
|
||||
it("should return formatted datetime for timestamp", () => {
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObjMock,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"attribute",
|
||||
"2023-10-10T10:10:10"
|
||||
);
|
||||
expect(result).toBe("October 10, 2023 at 10:10:10");
|
||||
});
|
||||
|
||||
it("should return JSON string for object value", () => {
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObjMock,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"attribute",
|
||||
{ key: "value" }
|
||||
);
|
||||
expect(result).toBe('{"key":"value"}');
|
||||
});
|
||||
|
||||
it("should return concatenated values for array", () => {
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObjMock,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"attribute",
|
||||
[1, 2, 3]
|
||||
);
|
||||
expect(result).toBe("1, 2, 3");
|
||||
});
|
||||
|
||||
it("should set special unit for weather domain", () => {
|
||||
const stateObj = {
|
||||
entity_id: "weather.test",
|
||||
attributes: {
|
||||
temperature: 42,
|
||||
},
|
||||
} as unknown as HassEntityBase;
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObj,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"temperature"
|
||||
);
|
||||
expect(result).toBe("42 °C");
|
||||
});
|
||||
|
||||
it("should set temperature unit for temperature attribute", () => {
|
||||
const stateObj = {
|
||||
entity_id: "sensor.test",
|
||||
attributes: {
|
||||
temperature: 42,
|
||||
},
|
||||
} as unknown as HassEntityBase;
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObj,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"temperature"
|
||||
);
|
||||
expect(result).toBe("42 °C");
|
||||
});
|
||||
|
||||
it("should return translation from translation key", () => {
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObjMock,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"attribute",
|
||||
"attributeValue"
|
||||
);
|
||||
expect(result).toBe("Localized Attribute Name");
|
||||
});
|
||||
|
||||
it("should return device class translation", () => {
|
||||
const stateObj = {
|
||||
entity_id: "media_player.test",
|
||||
attributes: {
|
||||
device_class: "media_player",
|
||||
volume_level: 0.42,
|
||||
},
|
||||
} as unknown as HassEntityBase;
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObj,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"attribute",
|
||||
"attributeValue"
|
||||
);
|
||||
expect(result).toBe("Localized Media Player Attribute Name");
|
||||
});
|
||||
|
||||
it("should return attribute value translation", () => {
|
||||
const stateObj = {
|
||||
entity_id: "media_player.test",
|
||||
attributes: {
|
||||
volume_level: 0.42,
|
||||
},
|
||||
} as unknown as HassEntityBase;
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObj,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"attribute",
|
||||
"attributeValue"
|
||||
);
|
||||
expect(result).toBe("Media Player Attribute Name");
|
||||
});
|
||||
|
||||
it("should return attribute value", () => {
|
||||
const stateObj = {
|
||||
entity_id: "media_player.test",
|
||||
attributes: {
|
||||
volume_level: 0.42,
|
||||
},
|
||||
} as unknown as HassEntityBase;
|
||||
const result = computeAttributeValueDisplay(
|
||||
localizeMock,
|
||||
stateObj,
|
||||
localeMock,
|
||||
configMock,
|
||||
entitiesMock,
|
||||
"attribute",
|
||||
"attributeValue2"
|
||||
);
|
||||
expect(result).toBe("attributeValue2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeAttributeNameDisplay", () => {
|
||||
it("should return localized name for attribute", () => {
|
||||
const localize = (key: string) => {
|
||||
if (
|
||||
key ===
|
||||
"component.light.entity.light.entity_translation_key.state_attributes.updated_at.name"
|
||||
) {
|
||||
return "Updated at";
|
||||
}
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
const stateObj = {
|
||||
entity_id: "light.test",
|
||||
attributes: {
|
||||
device_class: "light",
|
||||
},
|
||||
} as HassEntity;
|
||||
|
||||
const entities = {
|
||||
"light.test": {
|
||||
translation_key: "entity_translation_key",
|
||||
platform: "light",
|
||||
},
|
||||
} as unknown as HomeAssistant["entities"];
|
||||
|
||||
const result = computeAttributeNameDisplay(
|
||||
localize,
|
||||
stateObj,
|
||||
entities,
|
||||
"updated_at"
|
||||
);
|
||||
expect(result).toBe("Updated at");
|
||||
});
|
||||
|
||||
it("should return device class translation", () => {
|
||||
const localize = (key: string) => {
|
||||
if (
|
||||
key ===
|
||||
"component.light.entity_component.light.state_attributes.brightness.name"
|
||||
) {
|
||||
return "Brightness";
|
||||
}
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
const stateObj = {
|
||||
entity_id: "light.test",
|
||||
attributes: {
|
||||
device_class: "light",
|
||||
},
|
||||
} as HassEntity;
|
||||
|
||||
const entities = {} as unknown as HomeAssistant["entities"];
|
||||
|
||||
const result = computeAttributeNameDisplay(
|
||||
localize,
|
||||
stateObj,
|
||||
entities,
|
||||
"brightness"
|
||||
);
|
||||
expect(result).toBe("Brightness");
|
||||
});
|
||||
|
||||
it("should return default attribute name", () => {
|
||||
const localize = (key: string) => {
|
||||
if (
|
||||
key ===
|
||||
"component.light.entity_component._.state_attributes.brightness.name"
|
||||
) {
|
||||
return "Brightness";
|
||||
}
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
const stateObj = {
|
||||
entity_id: "light.test",
|
||||
attributes: {},
|
||||
} as HassEntity;
|
||||
|
||||
const entities = {} as unknown as HomeAssistant["entities"];
|
||||
|
||||
const result = computeAttributeNameDisplay(
|
||||
localize,
|
||||
stateObj,
|
||||
entities,
|
||||
"brightness"
|
||||
);
|
||||
expect(result).toBe("Brightness");
|
||||
});
|
||||
|
||||
it("should return capitalized attribute name", () => {
|
||||
const localize = () => "";
|
||||
|
||||
const stateObj = {
|
||||
entity_id: "light.test",
|
||||
attributes: {},
|
||||
} as HassEntity;
|
||||
|
||||
const entities = {} as unknown as HomeAssistant["entities"];
|
||||
|
||||
const result = computeAttributeNameDisplay(
|
||||
localize,
|
||||
stateObj,
|
||||
entities,
|
||||
"brightness__ip_id_mac_gps_GPS"
|
||||
);
|
||||
expect(result).toBe("Brightness IP ID MAC GPS GPS");
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,9 @@
|
||||
import { assert, describe, it, beforeEach } from "vitest";
|
||||
import { computeStateDisplay } from "../../../src/common/entity/compute_state_display";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import { assert, describe, it, beforeEach, expect } from "vitest";
|
||||
import {
|
||||
computeStateDisplay,
|
||||
computeStateDisplayFromEntityAttributes,
|
||||
} from "../../../src/common/entity/compute_state_display";
|
||||
import { UNKNOWN } from "../../../src/data/entity";
|
||||
import type { FrontendLocaleData } from "../../../src/data/translation";
|
||||
import {
|
||||
@@ -10,6 +14,7 @@ import {
|
||||
TimeZone,
|
||||
} from "../../../src/data/translation";
|
||||
import { demoConfig } from "../../../src/fake_data/demo_config";
|
||||
import type { EntityRegistryDisplayEntry } from "../../../src/data/entity_registry";
|
||||
|
||||
let localeData: FrontendLocaleData;
|
||||
|
||||
@@ -617,3 +622,85 @@ describe("computeStateDisplay", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeStateDisplayFromEntityAttributes with numeric device classes", () => {
|
||||
it("Should format duration sensor", () => {
|
||||
const result = computeStateDisplayFromEntityAttributes(
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
(() => {}) as any,
|
||||
{
|
||||
language: "en",
|
||||
} as FrontendLocaleData,
|
||||
[],
|
||||
{} as HassConfig,
|
||||
{
|
||||
display_precision: 2,
|
||||
} as EntityRegistryDisplayEntry,
|
||||
"number.test",
|
||||
{
|
||||
device_class: "duration",
|
||||
unit_of_measurement: "min",
|
||||
},
|
||||
"12"
|
||||
);
|
||||
expect(result).toBe("12.00 min");
|
||||
});
|
||||
it("Should format duration sensor with seconds", () => {
|
||||
const result = computeStateDisplayFromEntityAttributes(
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
(() => {}) as any,
|
||||
{
|
||||
language: "en",
|
||||
} as FrontendLocaleData,
|
||||
[],
|
||||
{} as HassConfig,
|
||||
undefined,
|
||||
"number.test",
|
||||
{
|
||||
device_class: "duration",
|
||||
unit_of_measurement: "s",
|
||||
},
|
||||
"12"
|
||||
);
|
||||
expect(result).toBe("12 s");
|
||||
});
|
||||
|
||||
it("Should format monetary device_class", () => {
|
||||
const result = computeStateDisplayFromEntityAttributes(
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
(() => {}) as any,
|
||||
{
|
||||
language: "en",
|
||||
} as FrontendLocaleData,
|
||||
[],
|
||||
{} as HassConfig,
|
||||
undefined,
|
||||
"number.test",
|
||||
{
|
||||
device_class: "monetary",
|
||||
unit_of_measurement: "$",
|
||||
},
|
||||
"12"
|
||||
);
|
||||
expect(result).toBe("12 $");
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeStateDisplayFromEntityAttributes datetime device calss", () => {
|
||||
it("Should format datetime sensor", () => {
|
||||
const result = computeStateDisplayFromEntityAttributes(
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
(() => {}) as any,
|
||||
{
|
||||
language: "en",
|
||||
} as FrontendLocaleData,
|
||||
[],
|
||||
{} as HassConfig,
|
||||
undefined,
|
||||
"button.test",
|
||||
{},
|
||||
"2020-01-01T12:00:00+00:00"
|
||||
);
|
||||
expect(result).toBe("January 1, 2020 at 12:00");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tsconfigPaths()],
|
||||
test: {
|
||||
environment: "jsdom", // to run in browser-like environment
|
||||
env: {
|
||||
|
||||
@@ -49,6 +49,36 @@
|
||||
"./node_modules/@lrnwebcomponents/simple-tooltip/custom-elements.json"
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"paths": {
|
||||
"lit/static-html": ["./node_modules/lit/static-html.js"],
|
||||
"lit/decorators": ["./node_modules/lit/decorators.js"],
|
||||
"lit/directive": ["./node_modules/lit/directive.js"],
|
||||
"lit/directives/until": ["./node_modules/lit/directives/until.js"],
|
||||
"lit/directives/class-map": [
|
||||
"./node_modules/lit/directives/class-map.js"
|
||||
],
|
||||
"lit/directives/style-map": [
|
||||
"./node_modules/lit/directives/style-map.js"
|
||||
],
|
||||
"lit/directives/if-defined": [
|
||||
"./node_modules/lit/directives/if-defined.js"
|
||||
],
|
||||
"lit/directives/guard": ["./node_modules/lit/directives/guard.js"],
|
||||
"lit/directives/cache": ["./node_modules/lit/directives/cache.js"],
|
||||
"lit/directives/repeat": ["./node_modules/lit/directives/repeat.js"],
|
||||
"lit/directives/live": ["./node_modules/lit/directives/live.js"],
|
||||
"lit/directives/keyed": ["./node_modules/lit/directives/keyed.js"],
|
||||
"lit/polyfill-support": ["./node_modules/lit/polyfill-support.js"],
|
||||
"@lit-labs/virtualizer/layouts/grid": [
|
||||
"./node_modules/@lit-labs/virtualizer/layouts/grid.js"
|
||||
],
|
||||
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver": [
|
||||
"./node_modules/@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js"
|
||||
],
|
||||
"@lit-labs/observers/resize-controller": [
|
||||
"./node_modules/@lit-labs/observers/resize-controller.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||