20240228.0 (#19908)

This commit is contained in:
Bram Kragten 2024-02-28 17:07:48 +01:00 committed by GitHub
commit 7475cb56a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
190 changed files with 6214 additions and 2638 deletions

View File

@ -2,12 +2,13 @@
"name": "Home Assistant Frontend",
"build": {
"dockerfile": "Dockerfile",
"context": "..",
"context": ".."
},
"appPort": "8124:8123",
"postCreateCommand": "sudo apt update && sudo apt upgrade -y && sudo apt install -y libpcap-dev",
"postStartCommand": "script/bootstrap",
"containerEnv": {
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}",
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
},
"customizations": {
"vscode": {
@ -16,7 +17,7 @@
"esbenp.prettier-vscode",
"runem.lit-plugin",
"github.vscode-pull-request-github",
"eamodio.gitlens",
"eamodio.gitlens"
],
"settings": {
"files.eol": "\n",
@ -27,17 +28,17 @@
"editor.renderWhitespace": "boundary",
"editor.rulers": [80],
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"files.trimTrailingWhitespace": true,
"terminal.integrated.shell.linux": "/usr/bin/zsh",
"gitlens.showWelcomeOnInstall": false,
"gitlens.showWhatsNewAfterUpgrades": false,
"workbench.startupEditor": "none",
},
},
},
"workbench.startupEditor": "none"
}
}
}
}

View File

@ -26,7 +26,7 @@ jobs:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.0.1
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn
@ -62,7 +62,7 @@ jobs:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.0.1
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@ -26,7 +26,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v4.0.1
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn
@ -60,7 +60,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v4.0.1
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn
@ -78,7 +78,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v4.0.1
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn
@ -89,7 +89,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.3.0
uses: actions/upload-artifact@v4.3.1
with:
name: frontend-bundle-stats
path: build/stats/*.json
@ -102,7 +102,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v4.0.1
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn
@ -113,7 +113,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.3.0
uses: actions/upload-artifact@v4.3.1
with:
name: supervisor-bundle-stats
path: build/stats/*.json

View File

@ -27,7 +27,7 @@ jobs:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.0.1
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn
@ -63,7 +63,7 @@ jobs:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.0.1
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v4.0.1
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v4.0.1
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.0.1
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn
@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@v4.3.0
uses: actions/upload-artifact@v4.3.1
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@v4.3.0
uses: actions/upload-artifact@v4.3.1
with:
name: translations
path: translations.tar.gz

View File

@ -34,7 +34,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.0.1
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@ -0,0 +1,13 @@
diff --git a/simple-tooltip.js b/simple-tooltip.js
index 78a87f6a223925f0e29fbedb268c85a142ec6985..3d686dd6a3d5a93342b4b01408089fc316b408ca 100644
--- a/simple-tooltip.js
+++ b/simple-tooltip.js
@@ -195,6 +195,8 @@ class SimpleTooltip extends LitElement {
.hidden {
position: absolute;
left: -10000px;
+ inset-inline-start: -10000px;
+ inset-inline-end: initial;
top: auto;
width: 1px;
height: 1px;

View File

@ -115,7 +115,9 @@ gulp.task("webpack-prod-app", () =>
gulp.task("webpack-dev-server-demo", () =>
runDevServer({
compiler: webpack(bothBuilds(createDemoConfig, { isProdBuild: false })),
compiler: webpack(
createDemoConfig({ isProdBuild: false, latestBuild: true })
),
contentBase: paths.demo_output_root,
port: 8090,
})
@ -131,7 +133,9 @@ gulp.task("webpack-prod-demo", () =>
gulp.task("webpack-dev-server-cast", () =>
runDevServer({
compiler: webpack(bothBuilds(createCastConfig, { isProdBuild: false })),
compiler: webpack(
createCastConfig({ isProdBuild: false, latestBuild: true })
),
contentBase: paths.cast_output_root,
port: 8080,
// Accessible from the network, because that's how Cast hits it.
@ -174,8 +178,9 @@ gulp.task("webpack-prod-hassio", () =>
gulp.task("webpack-dev-server-gallery", () =>
runDevServer({
// We don't use the es5 build, but the dev server will fuck up the publicPath if we don't
compiler: webpack(bothBuilds(createGalleryConfig, { isProdBuild: false })),
compiler: webpack(
createGalleryConfig({ isProdBuild: false, latestBuild: true })
),
contentBase: paths.gallery_output_root,
port: 8100,
listenHost: "0.0.0.0",

View File

@ -43,11 +43,6 @@ class HcLaunchScreen extends LitElement {
max-width: 80%;
object-fit: cover;
}
.status {
padding-right: 54px;
padding-inline-end: 54px;
padding-inline-start: initial;
}
`;
}
}

View File

@ -93,7 +93,7 @@ class HcLovelace extends LitElement {
}
private get _viewIndex() {
if (this.viewPath === null || this.viewPath === undefined) {
if (this.viewPath === null) {
return 0;
}
const selectedView = this.viewPath;

View File

@ -1,5 +1,5 @@
import { Button } from "@material/mwc-button";
import { html, LitElement, css, TemplateResult } from "lit";
import { html, LitElement, css, TemplateResult, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../../src/common/dom/fire_event";
@ -9,7 +9,7 @@ import "../../../src/components/ha-card";
class DemoBlackWhiteRow extends LitElement {
@property() title!: string;
@property() value!: any;
@property() value?: any;
@property({ type: Boolean }) public disabled = false;
@ -45,7 +45,9 @@ class DemoBlackWhiteRow extends LitElement {
</mwc-button>
</div>
</ha-card>
<pre>${JSON.stringify(this.value, undefined, 2)}</pre>
${this.value
? html`<pre>${JSON.stringify(this.value, undefined, 2)}</pre>`
: nothing}
</div>
</div>
`;

View File

@ -275,6 +275,14 @@ const SCHEMAS: {
selector: { color_temp: {} },
},
color_rgb: { name: "Color", selector: { color_rgb: {} } },
qr_code: {
name: "QR Code",
selector: { qr_code: { data: "https://home-assistant.io" } },
},
constant: {
name: "Constant",
selector: { constant: { value: true, label: "Yes!" } },
},
},
},
{
@ -501,7 +509,7 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
this.requestUpdate();
};
return html`
<demo-black-white-row .title=${info.name} .value=${this.data[idx]}>
<demo-black-white-row .title=${info.name}>
${["light", "dark"].map((slot) =>
Object.entries(info.input).map(
([key, value]) => html`
@ -534,8 +542,8 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
}
static styles = css`
ha-selector {
width: 60;
ha-settings-row {
--paper-item-body-two-line-min-height: 0;
}
.options {
max-width: 800px;

View File

@ -1263,6 +1263,7 @@ class HassioAddonInfo extends LitElement {
.card-actions {
justify-content: space-between;
display: flex;
direction: var(--direction);
}
.changelog {
display: contents;

View File

@ -154,12 +154,16 @@ class HassioHardwareDialog extends LitElement {
ha-icon-button {
position: absolute;
right: 16px;
inset-inline-end: 16px;
inset-inline-start: initial;
top: 10px;
text-decoration: none;
color: var(--primary-text-color);
}
h2 {
margin: 18px 42px 0 18px;
margin-inline-start: 18px;
margin-inline-end: 42px;
color: var(--primary-text-color);
}

View File

@ -1,7 +1,5 @@
import "@material/mwc-button/mwc-button";
import { mdiDelete, mdiDeleteOff } from "@mdi/js";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
@ -27,6 +25,8 @@ import type { HomeAssistant } from "../../../../src/types";
import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
import type { HaTextField } from "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-list-new";
import "../../../../src/components/ha-list-item-new";
@customElement("dialog-hassio-repositories")
class HassioRepositoriesDialog extends LitElement {
@ -106,44 +106,46 @@ class HassioRepositoriesDialog extends LitElement {
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="form">
${repositories.length
? repositories.map(
(repo) => html`
<paper-item class="option">
<paper-item-body three-line>
<div>${repo.name}</div>
<div secondary>${repo.maintainer}</div>
<div secondary>${repo.url}</div>
</paper-item-body>
<div class="delete">
<ha-icon-button
.label=${this._dialogParams!.supervisor.localize(
"dialog.repositories.remove"
)}
.disabled=${usedRepositories.includes(repo.slug)}
.slug=${repo.slug}
.path=${usedRepositories.includes(repo.slug)
? mdiDeleteOff
: mdiDelete}
@click=${this._removeRepository}
>
</ha-icon-button>
<simple-tooltip
animation-delay="0"
position="bottom"
offset="1"
>
${this._dialogParams!.supervisor.localize(
usedRepositories.includes(repo.slug)
? "dialog.repositories.used"
: "dialog.repositories.remove"
)}
</simple-tooltip>
</div>
</paper-item>
`
)
: html`<paper-item> No repositories </paper-item>`}
<ha-list-new>
${repositories.length
? repositories.map(
(repo) => html`
<ha-list-item-new class="option">
${repo.name}
<div slot="supporting-text">
<div>${repo.maintainer}</div>
<div>${repo.url}</div>
</div>
<div class="delete" slot="end">
<ha-icon-button
.label=${this._dialogParams!.supervisor.localize(
"dialog.repositories.remove"
)}
.disabled=${usedRepositories.includes(repo.slug)}
.slug=${repo.slug}
.path=${usedRepositories.includes(repo.slug)
? mdiDeleteOff
: mdiDelete}
@click=${this._removeRepository}
>
</ha-icon-button>
<simple-tooltip
animation-delay="0"
position="bottom"
offset="1"
>
${this._dialogParams!.supervisor.localize(
usedRepositories.includes(repo.slug)
? "dialog.repositories.used"
: "dialog.repositories.remove"
)}
</simple-tooltip>
</div>
</ha-list-item-new>
`
)
: html`<ha-list-item-new> No repositories </ha-list-item-new>`}
</ha-list-new>
<div class="layout horizontal bottom">
<ha-textfield
class="flex-auto"
@ -206,6 +208,9 @@ class HassioRepositoriesDialog extends LitElement {
div.delete ha-icon-button {
color: var(--error-color);
}
ha-list-item-new {
position: relative;
}
`,
];
}

View File

@ -31,9 +31,9 @@
"@codemirror/commands": "6.3.3",
"@codemirror/language": "6.10.1",
"@codemirror/legacy-modes": "6.3.3",
"@codemirror/search": "6.5.5",
"@codemirror/state": "6.4.0",
"@codemirror/view": "6.23.1",
"@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.24.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.12.2",
"@formatjs/intl-displaynames": "6.6.6",
@ -43,18 +43,18 @@
"@formatjs/intl-numberformat": "8.10.0",
"@formatjs/intl-pluralrules": "5.2.12",
"@formatjs/intl-relativetimeformat": "11.2.12",
"@fullcalendar/core": "6.1.10",
"@fullcalendar/daygrid": "6.1.10",
"@fullcalendar/interaction": "6.1.10",
"@fullcalendar/list": "6.1.10",
"@fullcalendar/luxon3": "6.1.10",
"@fullcalendar/timegrid": "6.1.10",
"@fullcalendar/core": "6.1.11",
"@fullcalendar/daygrid": "6.1.11",
"@fullcalendar/interaction": "6.1.11",
"@fullcalendar/list": "6.1.11",
"@fullcalendar/luxon3": "6.1.11",
"@fullcalendar/timegrid": "6.1.11",
"@lezer/highlight": "1.2.0",
"@lit-labs/context": "0.4.1",
"@lit-labs/motion": "1.0.7",
"@lit-labs/observers": "2.0.2",
"@lit-labs/virtualizer": "2.0.12",
"@lrnwebcomponents/simple-tooltip": "8.0.0",
"@lrnwebcomponents/simple-tooltip": "patch:@lrnwebcomponents/simple-tooltip@npm%3A8.0.0#~/.yarn/patches/@lrnwebcomponents-simple-tooltip-npm-8.0.0-77591f2e0c.patch",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-base": "0.27.0",
@ -80,7 +80,7 @@
"@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "=1.2.0",
"@material/web": "=1.3.0",
"@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47",
"@polymer/paper-item": "3.0.1",
@ -89,8 +89,8 @@
"@polymer/paper-toast": "3.0.1",
"@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.3.5",
"@vaadin/vaadin-themable-mixin": "24.3.5",
"@vaadin/combo-box": "24.3.6",
"@vaadin/vaadin-themable-mixin": "24.3.6",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@ -99,8 +99,9 @@
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"chart.js": "4.4.1",
"color-name": "2.0.0",
"comlink": "4.4.1",
"core-js": "3.35.1",
"core-js": "3.36.0",
"cropperjs": "1.6.1",
"date-fns": "2.30.0",
"date-fns-tz": "2.0.0",
@ -109,7 +110,7 @@
"element-internals-polyfill": "1.3.10",
"fuse.js": "7.0.0",
"google-timezones-json": "1.2.0",
"hls.js": "1.5.3",
"hls.js": "1.5.6",
"home-assistant-js-websocket": "9.1.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.5.11",
@ -118,7 +119,7 @@
"leaflet-draw": "1.0.4",
"lit": "2.8.0",
"luxon": "3.4.4",
"marked": "11.2.0",
"marked": "12.0.0",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2",
@ -155,7 +156,7 @@
"@babel/plugin-transform-runtime": "7.23.9",
"@babel/preset-env": "7.23.9",
"@babel/preset-typescript": "7.23.3",
"@bundle-stats/plugin-webpack-filter": "4.9.2",
"@bundle-stats/plugin-webpack-filter": "4.10.1",
"@koa/cors": "5.0.0",
"@lokalise/node-api": "12.1.0",
"@octokit/auth-oauth-device": "6.0.1",
@ -170,6 +171,7 @@
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.13",
"@types/chromecast-caf-sender": "1.0.8",
"@types/color-name": "1.1.3",
"@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9",
@ -179,19 +181,19 @@
"@types/mocha": "10.0.6",
"@types/qrcode": "1.5.5",
"@types/serve-handler": "6.1.4",
"@types/sortablejs": "1.15.7",
"@types/sortablejs": "1.15.8",
"@types/tar": "6.1.11",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "6.20.0",
"@typescript-eslint/parser": "6.20.0",
"@typescript-eslint/eslint-plugin": "7.0.2",
"@typescript-eslint/parser": "7.0.2",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3",
"babel-plugin-template-html-minifier": "4.1.0",
"chai": "5.0.3",
"chai": "5.1.0",
"del": "7.1.0",
"eslint": "8.56.0",
"eslint": "8.57.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.1.0",
"eslint-config-prettier": "9.1.0",
@ -200,31 +202,31 @@
"eslint-plugin-import": "2.29.1",
"eslint-plugin-lit": "1.11.0",
"eslint-plugin-lit-a11y": "4.1.2",
"eslint-plugin-unused-imports": "3.0.0",
"eslint-plugin-unused-imports": "3.1.0",
"eslint-plugin-wc": "2.0.4",
"fancy-log": "2.0.0",
"fs-extra": "11.2.0",
"glob": "10.3.10",
"gulp": "4.0.2",
"gulp-flatmap": "1.0.2",
"gulp-json-transform": "0.4.8",
"gulp-json-transform": "0.5.0",
"gulp-merge-json": "2.1.2",
"gulp-rename": "2.0.0",
"gulp-zopfli-green": "6.0.1",
"html-minifier-terser": "7.2.0",
"husky": "9.0.10",
"husky": "9.0.11",
"instant-mocha": "1.5.2",
"jszip": "3.10.1",
"lint-staged": "15.2.1",
"lint-staged": "15.2.2",
"lit-analyzer": "2.0.3",
"lodash.template": "4.5.0",
"magic-string": "0.30.6",
"magic-string": "0.30.7",
"map-stream": "0.0.7",
"mocha": "10.2.0",
"mocha": "10.3.0",
"object-hash": "3.0.0",
"open": "10.0.3",
"pinst": "3.0.0",
"prettier": "3.2.4",
"prettier": "3.2.5",
"rollup": "2.79.1",
"rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2",
@ -240,12 +242,12 @@
"typescript": "5.3.3",
"vinyl-buffer": "1.0.1",
"vinyl-source-stream": "2.0.0",
"webpack": "5.90.1",
"webpack": "5.90.3",
"webpack-cli": "5.1.4",
"webpack-dev-server": "4.15.1",
"webpack-dev-server": "5.0.2",
"webpack-manifest-plugin": "5.0.0",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "6.0.0",
"webpackbar": "6.0.1",
"workbox-build": "7.0.0"
},
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20240207.1"
version = "20240228.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

View File

@ -40,6 +40,7 @@ if [ -n "$ref" ]; then
echo "Installing Home Assistant core at ${ref}..."
python3 -m pip install --user --upgrade --src "$HOME/src" \
--editable "git+${coreURL}@${ref}#egg=homeassistant"
(cd ~/src/homeassistant && exec python3 -m script.translations develop --all)
fi
if [ ! -d "${WD}/config" ]; then

View File

@ -1,19 +1,25 @@
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
import { HomeAssistant } from "../../types";
import { ensureArray } from "../array/ensure-array";
import { isComponentLoaded } from "./is_component_loaded";
export const canShowPage = (hass: HomeAssistant, page: PageNavigation) =>
(isCore(page) || isLoadedIntegration(hass, page)) &&
!hideAdvancedPage(hass, page);
!hideAdvancedPage(hass, page) &&
isNotLoadedIntegration(hass, page);
const isLoadedIntegration = (hass: HomeAssistant, page: PageNavigation) =>
page.component
? isComponentLoaded(hass, page.component)
: page.components
? page.components.some((integration) =>
isComponentLoaded(hass, integration)
)
: true;
!page.component ||
ensureArray(page.component).some((integration) =>
isComponentLoaded(hass, integration)
);
const isNotLoadedIntegration = (hass: HomeAssistant, page: PageNavigation) =>
!page.not_component ||
!ensureArray(page.not_component).some((integration) =>
isComponentLoaded(hass, integration)
);
const isCore = (page: PageNavigation) => page.core;
const isAdvancedPage = (page: PageNavigation) => page.advancedOnly;
const userWantsAdvanced = (hass: HomeAssistant) => hass.userData?.showAdvanced;

View File

@ -1,8 +1,13 @@
import { MAIN_WINDOW_NAME } from "../../data/main_window";
export const mainWindow =
window.name === MAIN_WINDOW_NAME
? window
: parent.name === MAIN_WINDOW_NAME
? parent
: top!;
export const mainWindow = (() => {
try {
return window.name === MAIN_WINDOW_NAME
? window
: parent.name === MAIN_WINDOW_NAME
? parent
: top!;
} catch {
return window;
}
})();

View File

@ -53,9 +53,7 @@ export const computeAttributeValueDisplay = (
if (domain === "weather") {
unit = getWeatherUnit(config, stateObj as WeatherEntity, attribute);
}
if (TEMPERATURE_ATTRIBUTES.has(attribute)) {
} else if (TEMPERATURE_ATTRIBUTES.has(attribute)) {
unit = config.unit_system.temperature;
}

View File

@ -20,14 +20,14 @@ function findNestedItem(
}, obj);
}
export function nestedArrayMove<T>(
obj: T | T[],
export function nestedArrayMove<A>(
obj: A,
oldIndex: number,
newIndex: number,
oldPath?: ItemPath,
newPath?: ItemPath
): T | T[] {
const newObj = Array.isArray(obj) ? [...obj] : { ...obj };
): A {
const newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
const from = oldPath ? findNestedItem(newObj, oldPath) : newObj;
const to = newPath ? findNestedItem(newObj, newPath, true) : newObj;

View File

@ -5,10 +5,18 @@ import type {
ChartOptions,
TooltipModel,
} from "chart.js";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import {
css,
CSSResultGroup,
html,
nothing,
LitElement,
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../../common/dom/fire_event";
import { clamp } from "../../common/number/clamp";
import { HomeAssistant } from "../../types";
import { debounce } from "../../common/util/debounce";
@ -27,6 +35,11 @@ interface Tooltip
left: string;
}
export interface ChartDatasetExtra {
show_legend?: boolean;
legend_label?: string;
}
@customElement("ha-chart-base")
export class HaChartBase extends LitElement {
public chart?: Chart;
@ -38,6 +51,8 @@ export class HaChartBase extends LitElement {
@property({ attribute: false }) public data: ChartData = { datasets: [] };
@property({ attribute: false }) public extraData?: ChartDatasetExtra[];
@property({ attribute: false }) public options?: ChartOptions;
@property({ attribute: false }) public plugins?: any[];
@ -46,6 +61,8 @@ export class HaChartBase extends LitElement {
@property({ type: Number }) public paddingYAxis = 0;
@property({ type: Boolean }) public externalHidden = false;
@state() private _chartHeight?: number;
@state() private _tooltip?: Tooltip;
@ -58,6 +75,8 @@ export class HaChartBase extends LitElement {
private _paddingYAxisInternal = 0;
private _datasetOrder: number[] = [];
public disconnectedCallback() {
super.disconnectedCallback();
this._releaseCanvas();
@ -148,6 +167,29 @@ export class HaChartBase extends LitElement {
}
}
// put the legend labels in sorted order if provided
if (changedProps.has("data")) {
this._datasetOrder = this.data.datasets.map((_, index) => index);
if (this.data?.datasets.some((dataset) => dataset.order)) {
this._datasetOrder.sort(
(a, b) =>
(this.data.datasets[a].order || 0) -
(this.data.datasets[b].order || 0)
);
}
if (this.externalHidden) {
this._hiddenDatasets = new Set();
if (this.data?.datasets) {
this.data.datasets.forEach((dataset, index) => {
if (dataset.hidden) {
this._hiddenDatasets.add(index);
}
});
}
}
}
if (!this.hasUpdated || !this.chart) {
return;
}
@ -157,7 +199,7 @@ export class HaChartBase extends LitElement {
return;
}
if (changedProps.has("data")) {
if (this._hiddenDatasets.size) {
if (this._hiddenDatasets.size && !this.externalHidden) {
this.data.datasets.forEach((dataset, index) => {
dataset.hidden = this._hiddenDatasets.has(index);
});
@ -175,26 +217,32 @@ export class HaChartBase extends LitElement {
${this.options?.plugins?.legend?.display === true
? html`<div class="chartLegend">
<ul>
${this.data.datasets.map(
(dataset, index) =>
html`<li
.datasetIndex=${index}
@click=${this._legendClick}
class=${classMap({
hidden: this._hiddenDatasets.has(index),
})}
.title=${dataset.label}
>
<div
class="bullet"
style=${styleMap({
backgroundColor: dataset.backgroundColor as string,
borderColor: dataset.borderColor as string,
${this._datasetOrder.map((index) => {
const dataset = this.data.datasets[index];
return this.extraData?.[index]?.show_legend === false
? nothing
: html`<li
.datasetIndex=${index}
@click=${this._legendClick}
class=${classMap({
hidden: this._hiddenDatasets.has(index),
})}
></div>
<div class="label">${dataset.label}</div>
</li>`
)}
.title=${this.extraData?.[index]?.legend_label ??
dataset.label}
>
<div
class="bullet"
style=${styleMap({
backgroundColor: dataset.backgroundColor as string,
borderColor: dataset.borderColor as string,
})}
></div>
<div class="label">
${this.extraData?.[index]?.legend_label ??
dataset.label}
</div>
</li>`;
})}
</ul>
</div>`
: ""}
@ -211,9 +259,9 @@ export class HaChartBase extends LitElement {
height: `${
this.height ?? this._chartHeight ?? this.clientWidth / 2
}px`,
"padding-left": `${this._paddingYAxisInternal}`,
"padding-left": `${this._paddingYAxisInternal}px`,
"padding-right": 0,
"padding-inline-start": `${this._paddingYAxisInternal}`,
"padding-inline-start": `${this._paddingYAxisInternal}px`,
"padding-inline-end": 0,
})}
>
@ -339,9 +387,19 @@ export class HaChartBase extends LitElement {
if (this.chart.isDatasetVisible(index)) {
this.chart.setDatasetVisibility(index, false);
this._hiddenDatasets.add(index);
if (this.externalHidden) {
fireEvent(this, "dataset-hidden", {
index,
});
}
} else {
this.chart.setDatasetVisibility(index, true);
this._hiddenDatasets.delete(index);
if (this.externalHidden) {
fireEvent(this, "dataset-unhidden", {
index,
});
}
}
this.chart.update("none");
this.requestUpdate("_hiddenDatasets");
@ -486,4 +544,8 @@ declare global {
interface HTMLElementTagNameMap {
"ha-chart-base": HaChartBase;
}
interface HASSDomEvents {
"dataset-hidden": { index: number };
"dataset-unhidden": { index: number };
}
}

View File

@ -111,7 +111,7 @@ export class StateHistoryChartLine extends LitElement {
config: this.hass.config,
},
},
suggestedMin: this.startTime,
min: this.startTime,
suggestedMax: this.endTime,
ticks: {
maxRotation: 0,

View File

@ -114,7 +114,7 @@ export class StateHistoryChartTimeline extends LitElement {
config: this.hass.config,
},
},
suggestedMin: this.startTime,
min: this.startTime,
suggestedMax: this.endTime,
ticks: {
autoSkip: true,

View File

@ -233,16 +233,32 @@ export class StateHistoryCharts extends LitElement {
new Date().getTime() - 60 * 60 * this.hoursToShow * 1000
);
} else {
this._computedStartTime = new Date(
(this.historyData?.timeline ?? []).reduce(
(minTime, stateInfo) =>
Math.min(
minTime,
new Date(stateInfo.data[0].last_changed).getTime()
),
new Date().getTime()
)
let minTimeAll = (this.historyData?.timeline ?? []).reduce(
(minTime, stateInfo) =>
Math.min(
minTime,
new Date(stateInfo.data[0].last_changed).getTime()
),
new Date().getTime()
);
minTimeAll = (this.historyData?.line ?? []).reduce(
(minTimeLine, line) =>
Math.min(
minTimeLine,
line.data.reduce(
(minTimeData, data) =>
Math.min(
minTimeData,
new Date(data.states[0].last_changed).getTime()
),
minTimeLine
)
),
minTimeAll
);
this._computedStartTime = new Date(minTimeAll);
}
}
}

View File

@ -32,7 +32,11 @@ import {
} from "../../data/recorder";
import type { HomeAssistant } from "../../types";
import "./ha-chart-base";
import type { ChartResizeOptions, HaChartBase } from "./ha-chart-base";
import type {
ChartResizeOptions,
ChartDatasetExtra,
HaChartBase,
} from "./ha-chart-base";
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
mean: "mean",
@ -79,10 +83,14 @@ export class StatisticsChart extends LitElement {
@state() private _chartData: ChartData = { datasets: [] };
@state() private _chartDatasetExtra: ChartDatasetExtra[] = [];
@state() private _statisticIds: string[] = [];
@state() private _chartOptions?: ChartOptions;
@state() private _hiddenStats = new Set<string>();
@query("ha-chart-base") private _chart?: HaChartBase;
private _computedStyle?: CSSStyleDeclaration;
@ -96,6 +104,9 @@ export class StatisticsChart extends LitElement {
}
public willUpdate(changedProps: PropertyValues) {
if (changedProps.has("legendMode")) {
this._hiddenStats.clear();
}
if (
!this.hasUpdated ||
changedProps.has("unit") ||
@ -110,7 +121,8 @@ export class StatisticsChart extends LitElement {
changedProps.has("statisticsData") ||
changedProps.has("statTypes") ||
changedProps.has("chartType") ||
changedProps.has("hideLegend")
changedProps.has("hideLegend") ||
changedProps.has("_hiddenStats")
) {
this._generateData();
}
@ -145,14 +157,30 @@ export class StatisticsChart extends LitElement {
return html`
<ha-chart-base
externalHidden
.hass=${this.hass}
.data=${this._chartData}
.extraData=${this._chartDatasetExtra}
.options=${this._chartOptions}
.chartType=${this.chartType}
@dataset-hidden=${this._datasetHidden}
@dataset-unhidden=${this._datasetUnhidden}
></ha-chart-base>
`;
}
private _datasetHidden(ev) {
ev.stopPropagation();
this._hiddenStats.add(this._statisticIds[ev.detail.index]);
this.requestUpdate("_hiddenStats");
}
private _datasetUnhidden(ev) {
ev.stopPropagation();
this._hiddenStats.delete(this._statisticIds[ev.detail.index]);
this.requestUpdate("_hiddenStats");
}
private _createOptions(unit?: string) {
this._chartOptions = {
parsing: false,
@ -274,6 +302,7 @@ export class StatisticsChart extends LitElement {
let colorIndex = 0;
const statisticsData = Object.entries(this.statisticsData);
const totalDataSets: ChartDataset<"line">[] = [];
const totalDatasetExtras: ChartDatasetExtra[] = [];
const statisticIds: string[] = [];
let endTime: Date;
@ -324,6 +353,7 @@ export class StatisticsChart extends LitElement {
// The datasets for the current statistic
const statDataSets: ChartDataset<"line">[] = [];
const statDatasetExtras: ChartDatasetExtra[] = [];
const pushData = (
start: Date,
@ -384,9 +414,20 @@ export class StatisticsChart extends LitElement {
})
: this.statTypes;
let displayed_legend = false;
sortedTypes.forEach((type) => {
if (statisticsHaveType(stats, type)) {
const band = drawBands && (type === "min" || type === "max");
if (!this.hideLegend) {
const show_legend = hasMean
? type === "mean"
: displayed_legend === false;
statDatasetExtras.push({
legend_label: name,
show_legend,
});
displayed_legend = displayed_legend || show_legend;
}
statTypes.push(type);
statDataSets.push({
label: name
@ -408,6 +449,9 @@ export class StatisticsChart extends LitElement {
band && hasMean ? color + (this.hideLegend ? "00" : "7F") : color,
backgroundColor: band ? color + "3F" : color + "7F",
pointRadius: 0,
hidden: !this.hideLegend
? this._hiddenStats.has(statistic_id)
: false,
data: [],
// @ts-ignore
unit: meta?.unit_of_measurement,
@ -446,6 +490,7 @@ export class StatisticsChart extends LitElement {
// Concat two arrays
Array.prototype.push.apply(totalDataSets, statDataSets);
Array.prototype.push.apply(totalDatasetExtras, statDatasetExtras);
});
if (unit) {
@ -455,6 +500,7 @@ export class StatisticsChart extends LitElement {
this._chartData = {
datasets: totalDataSets,
};
this._chartDatasetExtra = totalDatasetExtras;
this._statisticIds = statisticIds;
}

View File

@ -205,7 +205,9 @@ export class TimelineController extends BarController {
const y = vScale.getPixelForValue(this.index);
const xStart = iScale.getPixelForValue(data.start.getTime());
const xStart = iScale.getPixelForValue(
Math.max(iScale.min, data.start.getTime())
);
const xEnd = iScale.getPixelForValue(data.end.getTime());
const width = xEnd - xStart;

View File

@ -49,7 +49,7 @@ export class TimeLineScale extends TimeScale {
max = isFinite(max) && !isNaN(max) ? max : +adapter.endOf(Date.now(), unit);
// Make sure that max is strictly higher than min (required by the lookup table)
this.min = Math.min(min, max - 1);
this.max = Math.max(min + 1, max);
this.min = adapter.parse(options.min, this) ?? Math.min(min, max - 1);
this.max = adapter.parse(options.max, this) ?? Math.max(min + 1, max);
}
}

View File

@ -44,6 +44,8 @@ class HaDataTableIcon extends LitElement {
div {
position: absolute;
right: 28px;
inset-inline-end: 28px;
inset-inline-start: initial;
z-index: 1002;
outline: none;
font-size: 10px;

View File

@ -32,7 +32,9 @@ export class StateBadge extends LitElement {
@property() public overrideImage?: string;
@property({ type: Boolean }) public stateColor = false;
// Cannot be a boolean attribute because undefined is treated different than
// false. When it is undefined, state is still colored for light entities.
@property({ attribute: false }) public stateColor?: boolean;
@property() public color?: string;
@ -70,7 +72,7 @@ export class StateBadge extends LitElement {
const domain = this.stateObj
? computeStateDomain(this.stateObj)
: undefined;
return this.stateColor || (domain === "light" && this.stateColor !== false);
return this.stateColor ?? domain === "light";
}
protected render() {

View File

@ -156,6 +156,7 @@ class HaClimateState extends LitElement {
.current {
color: var(--secondary-text-color);
direction: var(--direction);
}
.state-label {

View File

@ -127,9 +127,11 @@ export class HaControlButton extends LitElement {
opacity 180ms ease-in-out;
opacity: var(--control-button-background-opacity);
}
.button ::slotted(*) {
.button {
transition: color 180ms ease-in-out;
color: var(--control-button-icon-color);
}
.button ::slotted(*) {
pointer-events: none;
}
.button:disabled {

View File

@ -273,9 +273,13 @@ export class HaControlNumberButton extends LitElement {
}
.button.minus {
left: 0;
inset-inline-start: 0;
inset-inline-end: initial;
}
.button.plus {
right: 0;
inset-inline-start: initial;
inset-inline-end: 0;
}
.unit {
white-space: pre;

View File

@ -3,7 +3,6 @@ import { styles } from "@material/mwc-drawer/mwc-drawer.css";
import { css, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { mainWindow } from "../common/dom/get_main_window";
const blockingElements = (document as any).$blockingElements;
@ -34,7 +33,8 @@ export class HaDrawer extends DrawerBase {
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("direction")) {
if (mainWindow.document.dir === "rtl") {
this.mdcRoot.dir = this.direction;
if (this.direction === "rtl") {
this._rtlStyle = document.createElement("style");
this._rtlStyle.innerHTML = `
.mdc-drawer--animate {

View File

@ -54,7 +54,8 @@ export const computeInitialHaFormData = (
"icon" in selector ||
"template" in selector ||
"text" in selector ||
"theme" in selector
"theme" in selector ||
"object" in selector
) {
data[field.name] = "";
} else if ("number" in selector) {

View File

@ -1,11 +1,13 @@
import { FormfieldBase } from "@material/mwc-formfield/mwc-formfield-base";
import { styles } from "@material/mwc-formfield/mwc-formfield.css";
import { css } from "lit";
import { customElement } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-formfield")
export class HaFormfield extends FormfieldBase {
@property({ type: Boolean, reflect: true }) public disabled = false;
protected _labelClick() {
const input = this.input as HTMLInputElement | undefined;
if (!input) return;
@ -44,6 +46,9 @@ export class HaFormfield extends FormfieldBase {
padding-inline-start: 4px;
padding-inline-end: 0;
}
:host([disabled]) label {
color: var(--disabled-text-color);
}
`,
];
}

View File

@ -136,6 +136,8 @@ class HaMenuButton extends LitElement {
height: 12px;
top: 9px;
right: 7px;
inset-inline-end: 7px;
inset-inline-start: initial;
border-radius: 50%;
border: 2px solid var(--app-header-background-color);
}

View File

@ -19,7 +19,9 @@ class HaMetric extends LitElement {
<ha-settings-row>
<span slot="heading"> ${this.heading} </span>
<div slot="description" .title=${this.tooltip ?? ""}>
<span class="value"> ${roundedValue} % </span>
<span class="value">
<div>${roundedValue} %</div>
</span>
<ha-bar
class=${classMap({
"target-warning": roundedValue > 50,
@ -70,6 +72,10 @@ class HaMetric extends LitElement {
padding-inline-start: initial;
flex-shrink: 0;
}
.value > div {
direction: ltr;
text-align: var(--float-start);
}
`;
}
}

View File

@ -186,6 +186,8 @@ class HaQrScanner extends LitElement {
position: absolute;
bottom: 8px;
right: 8px;
inset-inline-end: 8px;
inset-inline-start: initial;
background: #727272b2;
color: white;
border-radius: 50%;

View File

@ -102,7 +102,10 @@ export class HaSelectSelector extends LitElement {
${this.label}
${options.map(
(item: SelectOption) => html`
<ha-formfield .label=${item.label}>
<ha-formfield
.label=${item.label}
.disabled=${item.disabled || this.disabled}
>
<ha-radio
.checked=${item.value === this.value}
.value=${item.value}

View File

@ -93,6 +93,8 @@ export class HaServiceControl extends LitElement {
@property({ type: Boolean, reflect: true }) public hidePicker = false;
@property({ type: Boolean }) public hideDescription = false;
@state() private _value!: this["value"];
@state() private _checkedKeys = new Set();
@ -373,7 +375,8 @@ export class HaServiceControl extends LitElement {
)) ||
serviceData?.description;
return html`${this.hidePicker
return html`
${this.hidePicker
? nothing
: html`<ha-service-picker
.hass=${this.hass}
@ -381,29 +384,33 @@ export class HaServiceControl extends LitElement {
.disabled=${this.disabled}
@value-changed=${this._serviceChanged}
></ha-service-picker>`}
<div class="description">
${description ? html`<p>${description}</p>` : ""}
${this._manifest
? html` <a
href=${this._manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this._manifest.domain}`
)
: this._manifest.documentation}
title=${this.hass.localize(
"ui.components.service-control.integration_doc"
)}
target="_blank"
rel="noreferrer"
>
<ha-icon-button
.path=${mdiHelpCircle}
class="help-icon"
></ha-icon-button>
</a>`
: ""}
</div>
${this.hideDescription
? nothing
: html`
<div class="description">
${description ? html`<p>${description}</p>` : ""}
${this._manifest
? html` <a
href=${this._manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this._manifest.domain}`
)
: this._manifest.documentation}
title=${this.hass.localize(
"ui.components.service-control.integration_doc"
)}
target="_blank"
rel="noreferrer"
>
<ha-icon-button
.path=${mdiHelpCircle}
class="help-icon"
></ha-icon-button>
</a>`
: nothing}
</div>
`}
${serviceData && "target" in serviceData
? html`<ha-settings-row .narrow=${this.narrow}>
${hasOptional
@ -517,7 +524,8 @@ export class HaServiceControl extends LitElement {
></ha-selector>
</ha-settings-row>`
: "";
})}`;
})}
`;
}
private _localizeValueCallback = (key: string) => {

View File

@ -114,11 +114,14 @@ class HaServicePicker extends LitElement {
if (!filter) {
return processedServices;
}
return processedServices.filter(
(service) =>
service.service.toLowerCase().includes(filter) ||
service.name?.toLowerCase().includes(filter)
);
const split_filter = filter.split(" ");
return processedServices.filter((service) => {
const lower_service_name = service.name.toLowerCase();
const lower_service = service.service.toLowerCase();
return split_filter.every(
(f) => lower_service_name.includes(f) || lower_service.includes(f)
);
});
}
);

View File

@ -1010,8 +1010,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}
.profile paper-icon-item {
padding-left: 4px;
margin-inline-start: 4px;
margin-inline-end: auto;
padding-inline-start: 4px;
padding-inline-end: auto;
}
.profile .item-text {
margin-left: 8px;
@ -1040,6 +1040,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
position: absolute;
bottom: 14px;
left: 26px;
inset-inline-start: 26px;
inset-inline-end: initial;
font-size: 0.65em;
}

View File

@ -14,9 +14,16 @@ declare global {
oldPath?: ItemPath;
newPath?: ItemPath;
};
"drag-start": undefined;
"drag-end": undefined;
}
}
export type HaSortableOptions = Omit<
SortableInstance.SortableOptions,
"onStart" | "onChoose" | "onEnd"
>;
@customElement("ha-sortable")
export class HaSortable extends LitElement {
private _sortable?: SortableInstance;
@ -36,14 +43,17 @@ export class HaSortable extends LitElement {
@property({ type: String, attribute: "handle-selector" })
public handleSelector?: string;
@property({ type: String, attribute: "group" })
public group?: string;
@property({ type: Number, attribute: "swap-threshold" })
public swapThreshold?: number;
@property({ type: String })
public group?: string | SortableInstance.GroupOptions;
@property({ type: Boolean, attribute: "invert-swap" })
public invertSwap?: boolean;
public invertSwap: boolean = false;
@property({ attribute: false })
public options?: HaSortableOptions;
@property({ type: Boolean })
public rollback: boolean = true;
protected updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has("disabled")) {
@ -114,26 +124,20 @@ export class HaSortable extends LitElement {
const options: SortableInstance.Options = {
animation: 150,
swapThreshold: 1,
...this.options,
onChoose: this._handleChoose,
onStart: this._handleStart,
onEnd: this._handleEnd,
};
if (this.draggableSelector) {
options.draggable = this.draggableSelector;
}
if (this.swapThreshold !== undefined) {
options.swapThreshold = this.swapThreshold;
}
if (this.invertSwap !== undefined) {
options.invertSwap = this.invertSwap;
}
if (this.handleSelector) {
options.handle = this.handleSelector;
}
if (this.draggableSelector) {
options.draggable = this.draggableSelector;
if (this.invertSwap !== undefined) {
options.invertSwap = this.invertSwap;
}
if (this.group) {
options.group = this.group;
@ -143,8 +147,9 @@ export class HaSortable extends LitElement {
}
private _handleEnd = async (evt: SortableEvent) => {
fireEvent(this, "drag-end");
// put back in original location
if ((evt.item as any).placeholder) {
if (this.rollback && (evt.item as any).placeholder) {
(evt.item as any).placeholder.replaceWith(evt.item);
delete (evt.item as any).placeholder;
}
@ -170,7 +175,12 @@ export class HaSortable extends LitElement {
});
};
private _handleStart = () => {
fireEvent(this, "drag-start");
};
private _handleChoose = (evt: SortableEvent) => {
if (!this.rollback) return;
(evt.item as any).placeholder = document.createComment("sort-placeholder");
evt.item.after((evt.item as any).placeholder);
};

View File

@ -90,7 +90,7 @@ export class HaTextField extends TextFieldBase {
padding-right: var(--text-field-suffix-padding-right, 0px);
padding-inline-start: var(--text-field-suffix-padding-left, 12px);
padding-inline-end: var(--text-field-suffix-padding-right, 0px);
direction: var(--direction);
direction: ltr;
}
.mdc-text-field--with-leading-icon {
padding-inline-start: var(--text-field-suffix-padding-left, 0px);
@ -199,7 +199,6 @@ export class HaTextField extends TextFieldBase {
// safari workaround - must be explicit
mainWindow.document.dir === "rtl"
? css`
.mdc-text-field__affix--suffix,
.mdc-text-field--with-leading-icon,
.mdc-text-field__icon--leading,
.mdc-floating-label,

View File

@ -223,7 +223,6 @@ class DialogMediaPlayerBrowse extends LitElement {
ha-media-player-browse {
--media-browser-max-height: calc(100vh - 65px);
direction: ltr;
}
:host(.opened) ha-media-player-browse {

View File

@ -879,6 +879,7 @@ export class HaMediaPlayerBrowse extends LitElement {
display: flex;
flex-direction: column;
position: relative;
direction: ltr;
}
ha-circular-progress {

View File

@ -18,6 +18,7 @@ export interface CloudPreferences {
google_enabled: boolean;
alexa_enabled: boolean;
remote_enabled: boolean;
remote_allow_remote_enable: boolean;
google_secure_devices_pin: string | undefined;
cloudhooks: { [webhookId: string]: CloudWebhook };
alexa_report_state: boolean;
@ -139,6 +140,7 @@ export const updateCloudPref = (
google_report_state?: CloudPreferences["google_report_state"];
google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"];
tts_default_voice?: CloudPreferences["tts_default_voice"];
remote_allow_remote_enable?: CloudPreferences["remote_allow_remote_enable"];
}
) =>
hass.callWS({

View File

@ -33,6 +33,7 @@ export interface DataEntryFlowStepForm {
description_placeholders?: Record<string, string>;
last_step: boolean | null;
preview?: string;
translation_domain?: string;
}
export interface DataEntryFlowStepExternal {
@ -42,6 +43,7 @@ export interface DataEntryFlowStepExternal {
step_id: string;
url: string;
description_placeholders: Record<string, string>;
translation_domain?: string;
}
export interface DataEntryFlowStepCreateEntry {
@ -53,6 +55,7 @@ export interface DataEntryFlowStepCreateEntry {
result?: ConfigEntry;
description: string;
description_placeholders?: Record<string, string>;
translation_domain?: string;
}
export interface DataEntryFlowStepAbort {
@ -61,6 +64,7 @@ export interface DataEntryFlowStepAbort {
handler: string;
reason: string;
description_placeholders?: Record<string, string>;
translation_domain?: string;
}
export interface DataEntryFlowStepProgress {
@ -70,6 +74,7 @@ export interface DataEntryFlowStepProgress {
step_id: string;
progress_action: string;
description_placeholders?: Record<string, string>;
translation_domain?: string;
}
export interface DataEntryFlowStepMenu {
@ -80,6 +85,7 @@ export interface DataEntryFlowStepMenu {
/** If array, use value to lookup translations in strings.json */
menu_options: string[] | Record<string, string>;
description_placeholders?: Record<string, string>;
translation_domain?: string;
}
export type DataEntryFlowStep =

View File

@ -331,6 +331,9 @@ export const getReferencedStatisticIds = (
}
}
}
if (!(includeTypes && !includeTypes.includes("device"))) {
statIDs.push(...prefs.device_consumption.map((d) => d.stat_consumption));
}
return statIDs;
};
@ -383,6 +386,7 @@ const getEnergyData = async (
"solar",
"battery",
"gas",
"device",
]);
const waterStatIds = getReferencedStatisticIds(prefs, info, ["water"]);
@ -777,7 +781,7 @@ export const getEnergyGasUnit = (
: "ft³";
};
export const getEnergyWaterUnit = (hass: HomeAssistant): string | undefined =>
export const getEnergyWaterUnit = (hass: HomeAssistant): string =>
hass.config.unit_system.length === "km" ? "L" : "gal";
export const energyStatisticHelpUrl =

View File

@ -81,7 +81,7 @@ export interface EntityHistoryState {
/** attributes */
a: { [key: string]: any };
/** last_changed; if set, also applies to lu */
lc: number;
lc?: number;
/** last_updated */
lu: number;
}
@ -419,17 +419,37 @@ const BLANK_UNIT = " ";
export const computeHistory = (
hass: HomeAssistant,
stateHistory: HistoryStates,
entityIds: string[],
localize: LocalizeFunc,
sensorNumericalDeviceClasses: string[],
splitDeviceClasses = false
): HistoryResult => {
const lineChartDevices: { [unit: string]: HistoryStates } = {};
const timelineDevices: TimelineEntity[] = [];
if (!stateHistory) {
const localStateHistory: HistoryStates = {};
// Create a limited history from stateObj if entity has no recorded history.
const allEntities = new Set([...entityIds, ...Object.keys(stateHistory)]);
allEntities.forEach((entity) => {
if (entity in stateHistory) {
localStateHistory[entity] = stateHistory[entity];
} else if (hass.states[entity]) {
localStateHistory[entity] = [
{
s: hass.states[entity].state,
a: hass.states[entity].attributes,
lu: new Date(hass.states[entity].last_updated).getTime() / 1000,
},
];
}
});
if (!localStateHistory) {
return { line: [], timeline: [] };
}
Object.keys(stateHistory).forEach((entityId) => {
const stateInfo = stateHistory[entityId];
Object.keys(localStateHistory).forEach((entityId) => {
const stateInfo = localStateHistory[entityId];
if (stateInfo.length === 0) {
return;
}

View File

@ -198,8 +198,9 @@ export const entryIcon = async (
if (entry.icon) {
return entry.icon;
}
const stateObj = hass.states[entry.entity_id] as HassEntity | undefined;
const domain = computeDomain(entry.entity_id);
return getEntityIcon(hass, domain, undefined, undefined, entry);
return getEntityIcon(hass, domain, stateObj, undefined, entry);
};
const getEntityIcon = async (

View File

@ -10,8 +10,10 @@ import {
LovelaceCard,
} from "../panels/lovelace/types";
import { HomeAssistant } from "../types";
import { LovelaceSectionConfig } from "./lovelace/config/section";
import { fetchConfig, LegacyLovelaceConfig } from "./lovelace/config/types";
import { LovelaceViewConfig } from "./lovelace/config/view";
import { HuiSection } from "../panels/lovelace/sections/hui-section";
export interface LovelacePanelConfig {
mode: "yaml" | "storage";
@ -24,10 +26,21 @@ export interface LovelaceViewElement extends HTMLElement {
index?: number;
cards?: Array<LovelaceCard | HuiErrorCard>;
badges?: LovelaceBadge[];
sections?: HuiSection[];
isStrategy: boolean;
setConfig(config: LovelaceViewConfig): void;
}
export interface LovelaceSectionElement extends HTMLElement {
hass?: HomeAssistant;
lovelace?: Lovelace;
viewIndex?: number;
index?: number;
cards?: Array<LovelaceCard | HuiErrorCard>;
isStrategy: boolean;
setConfig(config: LovelaceSectionConfig): void;
}
type LovelaceUpdatedEvent = HassEventBase & {
event_type: "lovelace_updated";
data: {

View File

@ -0,0 +1,26 @@
import type { LovelaceCardConfig } from "./card";
import type { LovelaceStrategyConfig } from "./strategy";
export interface LovelaceBaseSectionConfig {
title?: string;
}
export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
type?: string;
cards?: LovelaceCardConfig[];
}
export interface LovelaceStrategySectionConfig
extends LovelaceBaseSectionConfig {
strategy: LovelaceStrategyConfig;
}
export type LovelaceSectionRawConfig =
| LovelaceSectionConfig
| LovelaceStrategySectionConfig;
export function isStrategySection(
section: LovelaceSectionRawConfig
): section is LovelaceStrategySectionConfig {
return "strategy" in section;
}

View File

@ -1,5 +1,6 @@
import type { LovelaceBadgeConfig } from "./badge";
import type { LovelaceCardConfig } from "./card";
import type { LovelaceSectionRawConfig } from "./section";
import type { LovelaceStrategyConfig } from "./strategy";
export interface ShowViewConfig {
@ -23,6 +24,7 @@ export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
type?: string;
badges?: Array<string | LovelaceBadgeConfig>;
cards?: LovelaceCardConfig[];
sections?: LovelaceSectionRawConfig[];
}
export interface LovelaceStrategyViewConfig extends LovelaceBaseViewConfig {

View File

@ -35,6 +35,7 @@ export interface MatterNodeDiagnostics {
mac_address?: string;
available: boolean;
active_fabrics: MatterFabricData[];
active_fabric_index: number;
}
export interface MatterPingResult {

View File

@ -1,12 +1,17 @@
import { Connection, createCollection } from "home-assistant-js-websocket";
import { Store } from "home-assistant-js-websocket/dist/store";
import { stringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
import { AreaRegistryEntry } from "./area_registry";
const fetchAreaRegistry = (conn: Connection) =>
conn.sendMessagePromise<AreaRegistryEntry[]>({
type: "config/area_registry/list",
});
conn
.sendMessagePromise<AreaRegistryEntry[]>({
type: "config/area_registry/list",
})
.then((areas) =>
areas.sort((ent1, ent2) => stringCompare(ent1.name, ent2.name))
);
const subscribeAreaRegistryUpdates = (
conn: Connection,

View File

@ -44,7 +44,7 @@ export const showConfigFlowDialog = (
renderAbortDescription(hass, step) {
const description = hass.localize(
`component.${step.handler}.config.abort.${step.reason}`,
`component.${step.translation_domain || step.handler}.config.abort.${step.reason}`,
step.description_placeholders
);
@ -58,7 +58,7 @@ export const showConfigFlowDialog = (
renderShowFormStepHeader(hass, step) {
return (
hass.localize(
`component.${step.handler}.config.step.${step.step_id}.title`,
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.title`,
step.description_placeholders
) || hass.localize(`component.${step.handler}.title`)
);
@ -66,7 +66,7 @@ export const showConfigFlowDialog = (
renderShowFormStepDescription(hass, step) {
const description = hass.localize(
`component.${step.handler}.config.step.${step.step_id}.description`,
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.description`,
step.description_placeholders
);
return description
@ -84,7 +84,7 @@ export const showConfigFlowDialog = (
renderShowFormStepFieldHelper(hass, step, field) {
const description = hass.localize(
`component.${step.handler}.config.step.${step.step_id}.data_description.${field.name}`,
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.data_description.${field.name}`,
step.description_placeholders
);
return description
@ -95,7 +95,7 @@ export const showConfigFlowDialog = (
renderShowFormStepFieldError(hass, step, error) {
return (
hass.localize(
`component.${step.handler}.config.error.${error}`,
`component.${step.translation_domain || step.translation_domain || step.handler}.config.error.${error}`,
step.description_placeholders
) || error
);
@ -131,7 +131,7 @@ export const showConfigFlowDialog = (
renderExternalStepDescription(hass, step) {
const description = hass.localize(
`component.${step.handler}.config.${step.step_id}.description`,
`component.${step.translation_domain || step.handler}.config.${step.step_id}.description`,
step.description_placeholders
);
@ -155,7 +155,7 @@ export const showConfigFlowDialog = (
renderCreateEntryDescription(hass, step) {
const description = hass.localize(
`component.${step.handler}.config.create_entry.${
`component.${step.translation_domain || step.handler}.config.create_entry.${
step.description || "default"
}`,
step.description_placeholders
@ -190,7 +190,7 @@ export const showConfigFlowDialog = (
renderShowFormProgressDescription(hass, step) {
const description = hass.localize(
`component.${step.handler}.config.progress.${step.progress_action}`,
`component.${step.translation_domain || step.handler}.config.progress.${step.progress_action}`,
step.description_placeholders
);
return description
@ -210,7 +210,7 @@ export const showConfigFlowDialog = (
renderMenuDescription(hass, step) {
const description = hass.localize(
`component.${step.handler}.config.step.${step.step_id}.description`,
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.description`,
step.description_placeholders
);
return description
@ -222,7 +222,7 @@ export const showConfigFlowDialog = (
renderMenuOption(hass, step, option) {
return hass.localize(
`component.${step.handler}.config.step.${step.step_id}.menu_options.${option}`,
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.menu_options.${option}`,
step.description_placeholders
);
},

View File

@ -53,7 +53,7 @@ export const showOptionsFlowDialog = (
renderAbortDescription(hass, step) {
const description = hass.localize(
`component.${configEntry.domain}.options.abort.${step.reason}`,
`component.${step.translation_domain || configEntry.domain}.options.abort.${step.reason}`,
step.description_placeholders
);
@ -71,7 +71,7 @@ export const showOptionsFlowDialog = (
renderShowFormStepHeader(hass, step) {
return (
hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.title`,
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.title`,
step.description_placeholders
) || hass.localize(`ui.dialogs.options_flow.form.header`)
);
@ -79,7 +79,7 @@ export const showOptionsFlowDialog = (
renderShowFormStepDescription(hass, step) {
const description = hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.description`,
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.description`,
step.description_placeholders
);
return description
@ -101,7 +101,7 @@ export const showOptionsFlowDialog = (
renderShowFormStepFieldHelper(hass, step, field) {
const description = hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.data_description.${field.name}`,
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.data_description.${field.name}`,
step.description_placeholders
);
return description
@ -112,7 +112,7 @@ export const showOptionsFlowDialog = (
renderShowFormStepFieldError(hass, step, error) {
return (
hass.localize(
`component.${configEntry.domain}.options.error.${error}`,
`component.${step.translation_domain || configEntry.domain}.options.error.${error}`,
step.description_placeholders
) || error
);
@ -159,7 +159,7 @@ export const showOptionsFlowDialog = (
renderShowFormProgressDescription(hass, step) {
const description = hass.localize(
`component.${configEntry.domain}.options.progress.${step.progress_action}`,
`component.${step.translation_domain || configEntry.domain}.options.progress.${step.progress_action}`,
step.description_placeholders
);
return description
@ -183,7 +183,7 @@ export const showOptionsFlowDialog = (
renderMenuDescription(hass, step) {
const description = hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.description`,
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.description`,
step.description_placeholders
);
return description
@ -199,7 +199,7 @@ export const showOptionsFlowDialog = (
renderMenuOption(hass, step, option) {
return hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.menu_options.${option}`,
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.menu_options.${option}`,
step.description_placeholders
);
},

View File

@ -89,7 +89,12 @@ class DialogBox extends LitElement {
</div>
${confirmPrompt &&
html`
<mwc-button @click=${this._dismiss} slot="secondaryAction">
<mwc-button
@click=${this._dismiss}
slot="secondaryAction"
?dialogInitialFocus=${!this._params.prompt &&
this._params.destructive}
>
${this._params.dismissText
? this._params.dismissText
: this.hass.localize("ui.dialogs.generic.cancel")}
@ -97,7 +102,8 @@ class DialogBox extends LitElement {
`}
<mwc-button
@click=${this._confirm}
?dialogInitialFocus=${!this._params.prompt}
?dialogInitialFocus=${!this._params.prompt &&
!this._params.destructive}
slot="primaryAction"
class=${classMap({
destructive: this._params.destructive || false,

View File

@ -16,6 +16,8 @@ export class HaMoreInfoStateHeader extends LitElement {
@property({ attribute: false }) public stateOverride?: string;
@property({ attribute: false }) public changedOverride?: number;
@state() private _absoluteTime = false;
private _localizeState(): TemplateResult | string {
@ -50,13 +52,13 @@ export class HaMoreInfoStateHeader extends LitElement {
? html`
<ha-absolute-time
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
.datetime=${this.changedOverride ?? this.stateObj.last_changed}
></ha-absolute-time>
`
: html`
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
.datetime=${this.changedOverride ?? this.stateObj.last_changed}
capitalize
></ha-relative-time>
`}

View File

@ -322,6 +322,8 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
position: absolute;
top: -6px;
right: -6px;
inset-inline-end: -6px;
inset-inline-start: initial;
width: 20px;
height: 20px;
outline: none;

View File

@ -446,6 +446,8 @@ class LightRgbColorPicker extends LitElement {
position: absolute;
top: 0;
right: 0;
inset-inline-end: 0;
inset-inline-start: initial;
z-index: 1;
}

View File

@ -26,6 +26,7 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [
"light",
"lock",
"siren",
"script",
"switch",
"valve",
"water_heater",

View File

@ -34,7 +34,6 @@ class MoreInfoPerson extends LitElement {
: ""}
${!__DEMO__ &&
this.hass.user?.is_admin &&
this.stateObj.state === "not_home" &&
this.stateObj.attributes.latitude &&
this.stateObj.attributes.longitude
? html`

View File

@ -1,51 +1,220 @@
import { mdiPlay, mdiStop } from "@mdi/js";
import "@material/mwc-button";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-relative-time";
import "../../../components/ha-service-control";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/entity/state-info";
import { HomeAssistant } from "../../../types";
import { canRun, ScriptEntity } from "../../../data/script";
import { isUnavailableState } from "../../../data/entity";
import { computeObjectId } from "../../../common/entity/compute_object_id";
import { listenMediaQuery } from "../../../common/dom/media_query";
import "../components/ha-more-info-state-header";
@customElement("more-info-script")
class MoreInfoScript extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@property({ attribute: false }) public stateObj?: ScriptEntity;
@state() private _scriptData: Record<string, any> = {};
@state() private narrow = false;
private _unsubMediaQuery?: () => void;
public connectedCallback(): void {
super.connectedCallback();
this._unsubMediaQuery = listenMediaQuery(
"(max-width: 870px)",
(matches) => {
this.narrow = matches;
}
);
}
public disconnectedCallback(): void {
super.disconnectedCallback();
if (this._unsubMediaQuery) {
this._unsubMediaQuery();
this._unsubMediaQuery = undefined;
}
}
protected render() {
if (!this.hass || !this.stateObj) {
return nothing;
}
const stateObj = this.stateObj;
const fields =
this.hass.services.script[computeObjectId(this.stateObj.entity_id)]
?.fields;
const hasFields = fields && Object.keys(fields).length > 0;
const current = stateObj.attributes.current || 0;
const isQueued = stateObj.attributes.mode === "queued";
const isParallel = stateObj.attributes.mode === "parallel";
const hasQueue = isQueued && current > 1;
return html`
<hr />
<div class="flex">
<div>
${this.hass.localize(
"ui.dialogs.more_info_control.script.last_triggered"
)}:
</div>
${this.stateObj.attributes.last_triggered
<ha-more-info-state-header
.stateObj=${stateObj}
.hass=${this.hass}
.stateOverride=${current > 0
? isParallel && current > 1
? this.hass.localize("ui.card.script.running_parallel", {
active: current,
})
: this.hass.localize("ui.card.script.running_single")
: this.hass.localize("ui.card.script.idle")}
.changedOverride=${this.stateObj.attributes.last_triggered || 0}
></ha-more-info-state-header>
<div class=${`queue ${hasQueue ? "has-queue" : ""}`}>
${hasQueue
? html`
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.attributes.last_triggered}
capitalize
></ha-relative-time>
${this.hass.localize("ui.card.script.running_queued", {
queued: current - 1,
})}
`
: this.hass.localize("ui.components.relative_time.never")}
: ""}
</div>
${hasFields
? html`
<div class="fields">
<div class="title">
${this.hass.localize("ui.card.script.run_script")}
</div>
<ha-service-control
hidePicker
hideDescription
.hass=${this.hass}
.value=${this._scriptData}
.showAdvanced=${this.hass.userData?.showAdvanced}
.narrow=${this.narrow}
@value-changed=${this._scriptDataChanged}
></ha-service-control>
</div>
`
: nothing}
<ha-control-button-group>
<ha-control-button
@click=${this._cancelScript}
.disabled=${!current}
class="cancel-button"
>
<ha-svg-icon .path=${mdiStop}></ha-svg-icon>
${(isQueued || isParallel) && current > 1
? this.hass.localize("ui.card.script.cancel_all")
: this.hass.localize("ui.card.script.cancel")}
</ha-control-button>
<ha-control-button
class="run-button"
@click=${this._runScript}
.disabled=${isUnavailableState(stateObj.state) || !this._canRun()}
>
<ha-svg-icon .path=${mdiPlay}></ha-svg-icon>
${this.hass!.localize("ui.card.script.run")}
</ha-control-button>
</ha-control-button-group>
`;
}
protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (!changedProperties.has("stateObj")) {
return;
}
const oldState = changedProperties.get("stateObj") as
| HassEntity
| undefined;
const newState = this.stateObj;
if (newState && (!oldState || oldState.entity_id !== newState.entity_id)) {
this._scriptData = { service: newState.entity_id, data: {} };
}
}
private _cancelScript(ev: Event) {
ev.stopPropagation();
this._callService("turn_off");
}
private async _runScript(ev: Event) {
ev.stopPropagation();
this.hass.callService(
"script",
computeObjectId(this.stateObj!.entity_id),
this._scriptData.data
);
}
private _callService(service: string): void {
this.hass.callService("script", service, {
entity_id: this.stateObj!.entity_id,
});
}
private _scriptDataChanged(ev: CustomEvent): void {
this._scriptData = { ...this._scriptData, ...ev.detail.value };
}
private _canRun() {
if (
canRun(this.stateObj!) ||
// Restart can also always runs. Just cancels other run.
this.stateObj!.attributes.mode === "restart"
) {
return true;
}
return false;
}
static get styles(): CSSResultGroup {
return css`
.flex {
display: flex;
justify-content: space-between;
.queue {
visibility: hidden;
color: var(--secondary-text-color);
text-align: center;
margin-bottom: 16px;
height: 21px;
}
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 16px 0;
.queue.has-queue {
visibility: visible;
}
.fields {
padding: 16px;
border: 1px solid var(--divider-color);
border-radius: 8px;
margin-bottom: 16px;
}
.fields .title {
font-weight: bold;
}
ha-control-button ha-svg-icon {
z-index: -1;
margin-right: 4px;
}
ha-service-control {
--service-control-padding: 0;
--service-control-items-border-top: none;
}
`;
}

View File

@ -228,6 +228,7 @@ export class MoreInfoHistory extends LitElement {
this._stateHistory = computeHistory(
this.hass!,
combinedHistory,
[this.entityId],
this.hass!.localize,
sensorNumericDeviceClasses
);

View File

@ -14,6 +14,7 @@ import "./notification-item";
import "../../components/ha-header-bar";
import "../../components/ha-drawer";
import type { HaDrawer } from "../../components/ha-drawer";
import { computeRTLDirection } from "../../common/util/compute_rtl";
@customElement("notification-drawer")
export class HuiNotificationDrawer extends LitElement {
@ -92,7 +93,12 @@ export class HuiNotificationDrawer extends LitElement {
});
return html`
<ha-drawer type="modal" open @MDCDrawer:closed=${this._dialogClosed}>
<ha-drawer
type="modal"
open
@MDCDrawer:closed=${this._dialogClosed}
.direction=${computeRTLDirection(this.hass)}
>
<ha-header-bar>
<div slot="title">
${this.hass.localize("ui.notification_drawer.title")}

View File

@ -647,6 +647,8 @@ export class HaVoiceCommandDialog extends LitElement {
position: absolute;
--mdc-icon-size: 16px;
right: 5px;
inset-inline-end: 5px;
inset-inline-start: initial;
top: 0px;
}

View File

@ -35,13 +35,22 @@ interface EMOutgoingMessageConfigGet extends EMMessage {
type: "config/get";
}
interface EMOutgoingMessageScanQRCode extends EMMessage {
type: "qr_code/scan";
interface EMOutgoingMessageBarCodeScan extends EMMessage {
type: "bar_code/scan";
title: string;
description: string;
alternative_option_label?: string;
}
interface EMOutgoingMessageBarCodeClose extends EMMessage {
type: "bar_code/close";
}
interface EMOutgoingMessageBarCodeNotify extends EMMessage {
type: "bar_code/notify";
message: string;
}
interface EMOutgoingMessageMatterCommission extends EMMessage {
type: "matter/commission";
}
@ -55,13 +64,6 @@ type EMOutgoingMessageWithAnswer = {
request: EMOutgoingMessageConfigGet;
response: ExternalConfig;
};
"qr_code/scan": {
request: EMOutgoingMessageScanQRCode;
response:
| EMIncomingMessageQRCodeResponseCanceled
| EMIncomingMessageQRCodeResponseAlternativeOptions
| EMIncomingMessageQRCodeResponseScanResult;
};
};
interface EMOutgoingMessageExoplayerPlayHLS extends EMMessage {
@ -124,20 +126,23 @@ interface EMOutgoingMessageAssistShow extends EMMessage {
}
type EMOutgoingMessageWithoutAnswer =
| EMOutgoingMessageHaptic
| EMOutgoingMessageConnectionStatus
| EMMessageResultError
| EMMessageResultSuccess
| EMOutgoingMessageAppConfiguration
| EMOutgoingMessageTagWrite
| EMOutgoingMessageSidebarShow
| EMOutgoingMessageAssistShow
| EMOutgoingMessageBarCodeClose
| EMOutgoingMessageBarCodeNotify
| EMOutgoingMessageBarCodeScan
| EMOutgoingMessageConnectionStatus
| EMOutgoingMessageExoplayerPlayHLS
| EMOutgoingMessageExoplayerResize
| EMOutgoingMessageExoplayerStop
| EMOutgoingMessageThemeUpdate
| EMMessageResultSuccess
| EMMessageResultError
| EMOutgoingMessageHaptic
| EMOutgoingMessageImportThreadCredentials
| EMOutgoingMessageMatterCommission
| EMOutgoingMessageImportThreadCredentials;
| EMOutgoingMessageSidebarShow
| EMOutgoingMessageTagWrite
| EMOutgoingMessageThemeUpdate;
interface EMIncomingMessageRestart {
id: number;
@ -172,17 +177,39 @@ interface EMIncomingMessageShowAutomationEditor {
};
}
export interface EMIncomingMessageQRCodeResponseCanceled {
action: "canceled";
export interface EMIncomingMessageBarCodeScanResult {
id: number;
type: "command";
command: "bar_code/scan_result";
payload: {
// A string decoded from the barcode data.
rawValue: string;
// https://developer.mozilla.org/en-US/docs/Web/API/Barcode_Detection_API#supported_barcode_formats
format:
| "aztec"
| "code_128"
| "code_39"
| "code_93"
| "codabar"
| "data_matrix"
| "ean_13"
| "ean_8"
| "itf"
| "pdf417"
| "qr_code"
| "upc_a"
| "upc_e"
| "unknown";
};
}
export interface EMIncomingMessageQRCodeResponseAlternativeOptions {
action: "alternative_options";
}
export interface EMIncomingMessageQRCodeResponseScanResult {
action: "scan_result";
result: string;
export interface EMIncomingMessageBarCodeScanAborted {
id: number;
type: "command";
command: "bar_code/aborted";
payload: {
reason: "canceled" | "alternative_options";
};
}
export type EMIncomingMessageCommands =
@ -190,7 +217,9 @@ export type EMIncomingMessageCommands =
| EMIncomingMessageShowNotifications
| EMIncomingMessageToggleSidebar
| EMIncomingMessageShowSidebar
| EMIncomingMessageShowAutomationEditor;
| EMIncomingMessageShowAutomationEditor
| EMIncomingMessageBarCodeScanResult
| EMIncomingMessageBarCodeScanAborted;
type EMIncomingMessage =
| EMMessageResultSuccess
@ -207,7 +236,7 @@ export interface ExternalConfig {
canCommissionMatter: boolean;
canImportThreadCredentials: boolean;
hasAssist: boolean;
hasQRScanner: number;
hasBarCodeScanner: number;
}
export class ExternalMessaging {

View File

@ -377,6 +377,8 @@ export class HaTabsSubpageDataTable extends LitElement {
color: var(--text-primary-color);
position: absolute;
right: 0;
inset-inline-end: 0;
inset-inline-start: initial;
top: 4px;
font-size: 0.65em;
}

View File

@ -10,7 +10,6 @@ import {
import { customElement, eventOptions, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { restoreScroll } from "../common/decorators/restore-scroll";
import { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-icon-button-arrow-prev";
@ -19,13 +18,14 @@ import "../components/ha-svg-icon";
import "../components/ha-tab";
import { HomeAssistant, Route } from "../types";
import { haStyleScrollbar } from "../resources/styles";
import { canShowPage } from "../common/config/can_show_page";
export interface PageNavigation {
path: string;
translationKey?: string;
component?: string;
components?: string[];
component?: string | string[];
name?: string;
not_component?: string | string[];
core?: boolean;
advancedOnly?: boolean;
iconPath?: string;
@ -66,19 +66,12 @@ class HassTabsSubpage extends LitElement {
(
tabs: PageNavigation[],
activeTab: PageNavigation | undefined,
showAdvanced: boolean | undefined,
_components,
_language,
_narrow,
localizeFunc
) => {
const shownTabs = tabs.filter(
(page) =>
(!page.component ||
page.core ||
isComponentLoaded(this.hass, page.component)) &&
(!page.advancedOnly || showAdvanced)
);
const shownTabs = tabs.filter((page) => canShowPage(this.hass, page));
if (shownTabs.length < 2) {
if (shownTabs.length === 1) {
@ -127,7 +120,6 @@ class HassTabsSubpage extends LitElement {
const tabs = this._getTabs(
this.tabs,
this._activeTab,
this.hass.userData?.showAdvanced,
this.hass.config.components,
this.hass.language,
this.narrow,

View File

@ -15,6 +15,7 @@ import "../components/ha-drawer";
import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer";
import type { HomeAssistant, Route } from "../types";
import "./partial-panel-resolver";
import { computeRTLDirection } from "../common/util/compute_rtl";
declare global {
// for fire event
@ -61,6 +62,7 @@ export class HomeAssistantMain extends LitElement {
<ha-drawer
.type=${sidebarNarrow ? "modal" : ""}
.open=${sidebarNarrow ? this._drawerOpen : undefined}
.direction=${computeRTLDirection(this.hass)}
@MDCDrawer:closed=${this._drawerClosed}
>
<ha-sidebar

View File

@ -499,6 +499,8 @@ class OnboardingLocation extends LitElement {
position: absolute;
top: 10px;
right: 10px;
inset-inline-end: 10px;
inset-inline-start: initial;
--mdc-icon-button-size: 36px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
@ -509,6 +511,8 @@ class OnboardingLocation extends LitElement {
ha-textfield > ha-circular-progress {
position: relative;
left: 12px;
inset-inline-start: 12px;
inset-inline-end: initial;
}
ha-locations-editor {
display: block;

View File

@ -188,7 +188,7 @@ export class HAFullCalendar extends LitElement {
</ha-icon-button-next>
</div>
</div>
<div class="controls">
<div class="controls buttons">
<mwc-button
outlined
class="today"
@ -480,6 +480,16 @@ export class HAFullCalendar extends LitElement {
width: 100%;
}
.buttons {
display: flex;
flex-wrap: wrap;
}
.buttons > * {
margin-bottom: 5px;
box-sizing: border-box;
}
.today {
margin-right: 20px;
margin-inline-end: 20px;

View File

@ -568,7 +568,7 @@ class HaConfigAreaPage extends LitElement {
<a
href=${ifDefined(
entityState.attributes.id
? `/config/automation/edit/${entityState.attributes.id}`
? `/config/automation/edit/${encodeURIComponent(entityState.attributes.id)}`
: undefined
)}
>
@ -710,6 +710,8 @@ class HaConfigAreaPage extends LitElement {
position: absolute;
top: 4px;
right: 4px;
inset-inline-end: 4px;
inset-inline-start: initial;
display: none;
}
.img-container:hover .img-edit-btn {
@ -736,6 +738,11 @@ class HaConfigAreaPage extends LitElement {
padding: 16px;
color: var(--secondary-text-color);
}
mwc-button > ha-svg-icon {
margin-inline-start: 0;
margin-inline-end: 8px;
}
`,
];
}

View File

@ -203,12 +203,14 @@ export default class HaAutomationAction extends LitElement {
}
private _moveUp(ev) {
ev.stopPropagation();
const index = (ev.target as any).index;
const newIndex = index - 1;
this._move(index, newIndex);
}
private _moveDown(ev) {
ev.stopPropagation();
const index = (ev.target as any).index;
const newIndex = index + 1;
this._move(index, newIndex);

View File

@ -536,6 +536,8 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
(!this._group ||
items.find((item) => item.key === this._params!.clipboardItem))
? html`<ha-list-item-new
interactive
type="button"
class="paste"
.value=${PASTE_VALUE}
@click=${this._selected}
@ -543,7 +545,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
${this.hass.localize(
`ui.panel.config.automation.editor.${this._params.type}s.paste`
)}
<span slot="secondary"
<span slot="supporting-text"
>${this.hass.localize(
// @ts-ignore
`ui.panel.config.automation.editor.${this._params.type}s.type.${this._params.clipboardItem}.label`

View File

@ -1,56 +1,23 @@
import "@material/mwc-button/mwc-button";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { nestedArrayMove } from "../../../common/util/array-move";
import { html } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-alert";
import "../../../components/ha-blueprint-picker";
import "../../../components/ha-card";
import "../../../components/ha-circular-progress";
import "../../../components/ha-markdown";
import "../../../components/ha-selector/ha-selector";
import "../../../components/ha-settings-row";
import { BlueprintAutomationConfig } from "../../../data/automation";
import {
BlueprintOrError,
Blueprints,
fetchBlueprints,
} from "../../../data/blueprint";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import { fetchBlueprints } from "../../../data/blueprint";
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
@customElement("blueprint-automation-editor")
export class HaBlueprintAutomationEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean, reflect: true }) public narrow = false;
export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
@property({ attribute: false }) public config!: BlueprintAutomationConfig;
@property({ attribute: false }) public stateObj?: HassEntity;
@state() private _blueprints?: Blueprints;
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._getBlueprints();
}
private get _blueprint(): BlueprintOrError | undefined {
if (!this._blueprints) {
return undefined;
}
return this._blueprints[this.config.use_blueprint.path];
protected get _config(): BlueprintAutomationConfig {
return this.config;
}
protected render() {
const blueprint = this._blueprint;
return html`
${this.disabled
? html`<ha-alert alert-type="warning">
@ -77,167 +44,14 @@ export class HaBlueprintAutomationEditor extends LitElement {
${this.config.description
? html`<p class="description">${this.config.description}</p>`
: ""}
<ha-card
outlined
class="blueprint"
.header=${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.header"
)}
>
<div class="blueprint-picker-container">
${this._blueprints
? Object.keys(this._blueprints).length
? html`
<ha-blueprint-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.blueprint_to_use"
)}
.blueprints=${this._blueprints}
.value=${this.config.use_blueprint.path}
.disabled=${this.disabled}
@value-changed=${this._blueprintChanged}
></ha-blueprint-picker>
`
: this.hass.localize(
"ui.panel.config.automation.editor.blueprint.no_blueprints"
)
: html`<ha-circular-progress indeterminate></ha-circular-progress>`}
</div>
${this.config.use_blueprint.path
? blueprint && "error" in blueprint
? html`<p class="warning padding">
There is an error in this Blueprint: ${blueprint.error}
</p>`
: html`${blueprint?.metadata.description
? html`<ha-markdown
class="card-content"
breaks
.content=${blueprint.metadata.description}
></ha-markdown>`
: ""}
${blueprint?.metadata?.input &&
Object.keys(blueprint.metadata.input).length
? Object.entries(blueprint.metadata.input).map(
([key, value]) => {
const selector = value?.selector ?? { text: undefined };
const type = Object.keys(selector)[0];
const enhancedSelector = [
"action",
"condition",
"trigger",
].includes(type)
? {
[type]: {
...selector[type],
path: [key],
},
}
: selector;
return html`<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">${value?.name || key}</span>
<ha-markdown
slot="description"
class="card-content"
breaks
.content=${value?.description}
></ha-markdown>
${html`<ha-selector
.hass=${this.hass}
.selector=${enhancedSelector}
.key=${key}
.disabled=${this.disabled}
.required=${value?.default === undefined}
.placeholder=${value?.default}
.value=${this.config.use_blueprint.input &&
key in this.config.use_blueprint.input
? this.config.use_blueprint.input[key]
: value?.default}
@value-changed=${this._inputChanged}
@item-moved=${this._itemMoved}
></ha-selector>`}
</ha-settings-row>`;
}
)
: html`<p class="padding">
${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.no_inputs"
)}
</p>`}`
: ""}
</ha-card>
${this.renderCard()}
`;
}
private async _getBlueprints() {
protected async _getBlueprints() {
this._blueprints = await fetchBlueprints(this.hass, "automation");
}
private _blueprintChanged(ev) {
ev.stopPropagation();
if (this.config.use_blueprint.path === ev.detail.value) {
return;
}
fireEvent(this, "value-changed", {
value: {
...this.config,
use_blueprint: {
path: ev.detail.value,
},
},
});
}
private _inputChanged(ev) {
ev.stopPropagation();
const target = ev.target as any;
const key = target.key;
const value = ev.detail ? ev.detail.value : target.value;
if (
(this.config.use_blueprint.input &&
this.config.use_blueprint.input[key] === value) ||
(!this.config.use_blueprint.input && value === "")
) {
return;
}
const input = { ...this.config.use_blueprint.input, [key]: value };
fireEvent(this, "value-changed", {
value: {
...this.config,
use_blueprint: {
...this.config.use_blueprint,
input,
},
},
});
}
private _itemMoved(ev) {
ev.stopPropagation();
const { oldIndex, newIndex, oldPath, newPath } = ev.detail;
const input = nestedArrayMove(
this.config.use_blueprint.input,
oldIndex,
newIndex,
oldPath,
newPath
);
fireEvent(this, "value-changed", {
value: {
...this.config,
use_blueprint: {
...this.config.use_blueprint,
input,
},
},
});
}
private async _enable(): Promise<void> {
if (!this.hass || !this.stateObj) {
return;
@ -246,69 +60,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
entity_id: this.stateObj.entity_id,
});
}
private _duplicate() {
fireEvent(this, "duplicate");
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
display: block;
}
ha-card.blueprint {
margin: 0 auto;
}
.padding {
padding: 16px;
}
.link-button-row {
padding: 14px;
}
.blueprint-picker-container {
padding: 0 16px 16px;
}
ha-textfield,
ha-blueprint-picker {
display: block;
}
h3 {
margin: 16px;
}
.introduction {
margin-top: 0;
margin-bottom: 12px;
}
.introduction a {
color: var(--primary-color);
}
p {
margin-bottom: 0;
}
.description {
margin-bottom: 16px;
}
ha-settings-row {
--paper-time-input-justify-content: flex-end;
--settings-row-content-width: 100%;
--settings-row-prefix-display: contents;
border-top: 1px solid var(--divider-color);
}
ha-alert {
margin-bottom: 16px;
display: block;
}
ha-alert.re-order {
border-radius: var(--ha-card-border-radius, 12px);
overflow: hidden;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"blueprint-automation-editor": HaBlueprintAutomationEditor;

View File

@ -227,12 +227,14 @@ export default class HaAutomationCondition extends LitElement {
}
private _moveUp(ev) {
ev.stopPropagation();
const index = (ev.target as any).index;
const newIndex = index - 1;
this._move(index, newIndex);
}
private _moveDown(ev) {
ev.stopPropagation();
const index = (ev.target as any).index;
const newIndex = index + 1;
this._move(index, newIndex);

View File

@ -172,7 +172,11 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
</ha-list-item>
${stateObj && this._config && this.narrow
? html`<a href="/config/automation/trace/${this._config.id}">
? html`<a
href="/config/automation/trace/${encodeURIComponent(
this._config.id!
)}"
>
<ha-list-item graphic="icon">
${this.hass.localize(
"ui.panel.config.automation.editor.show_trace"
@ -563,7 +567,9 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
if (this._config?.id) {
const result = await this.confirmUnsavedChanged();
if (result) {
navigate(`/config/automation/trace/${this._config.id}`);
navigate(
`/config/automation/trace/${encodeURIComponent(this._config.id)}`
);
}
}
}

View File

@ -435,7 +435,9 @@ class HaAutomationPicker extends LitElement {
});
return;
}
navigate(`/config/automation/trace/${automation.attributes.id}`);
navigate(
`/config/automation/trace/${encodeURIComponent(automation.attributes.id)}`
);
}
private async _toggle(automation): Promise<void> {
@ -530,9 +532,11 @@ class HaAutomationPicker extends LitElement {
);
if (automation?.attributes.id) {
navigate(`/config/automation/edit/${automation.attributes.id}`);
navigate(
`/config/automation/edit/${encodeURIComponent(automation.attributes.id)}`
);
} else {
navigate(`/config/automation/show/${ev.detail.id}`);
navigate(`/config/automation/show/${encodeURIComponent(ev.detail.id)}`);
}
}

View File

@ -106,7 +106,9 @@ export class HaAutomationTrace extends LitElement {
? html`
<a
class="trace-link"
href="/config/automation/edit/${stateObj.attributes.id}"
href="/config/automation/edit/${encodeURIComponent(
stateObj.attributes.id
)}"
slot="toolbar-icon"
>
<mwc-button>
@ -140,7 +142,9 @@ export class HaAutomationTrace extends LitElement {
? html`
<a
class="trace-link"
href="/config/automation/edit/${stateObj.attributes.id}"
href="/config/automation/edit/${encodeURIComponent(
stateObj.attributes.id
)}"
>
<mwc-list-item graphic="icon">
${this.hass.localize(

View File

@ -180,12 +180,14 @@ export default class HaAutomationTrigger extends LitElement {
}
private _moveUp(ev) {
ev.stopPropagation();
const index = (ev.target as any).index;
const newIndex = index - 1;
this._move(index, newIndex);
}
private _moveDown(ev) {
ev.stopPropagation();
const index = (ev.target as any).index;
const newIndex = index + 1;
this._move(index, newIndex);

View File

@ -0,0 +1,275 @@
import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { nestedArrayMove } from "../../../common/util/array-move";
import "../../../components/ha-alert";
import "../../../components/ha-blueprint-picker";
import "../../../components/ha-card";
import "../../../components/ha-circular-progress";
import "../../../components/ha-markdown";
import "../../../components/ha-selector/ha-selector";
import "../../../components/ha-settings-row";
import { BlueprintAutomationConfig } from "../../../data/automation";
import { BlueprintOrError, Blueprints } from "../../../data/blueprint";
import { BlueprintScriptConfig } from "../../../data/script";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
@customElement("blueprint-generic-editor")
export abstract class HaBlueprintGenericEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean, reflect: true }) public narrow = false;
@state() protected _blueprints?: Blueprints;
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._getBlueprints();
}
protected get _blueprint(): BlueprintOrError | undefined {
if (!this._blueprints) {
return undefined;
}
return this._blueprints[this._config.use_blueprint.path];
}
protected abstract get _config():
| BlueprintAutomationConfig
| BlueprintScriptConfig;
protected renderCard() {
const blueprint = this._blueprint;
return html`
<ha-card
outlined
class="blueprint"
.header=${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.header"
)}
>
<div class="blueprint-picker-container">
${this._blueprints
? Object.keys(this._blueprints).length
? html`
<ha-blueprint-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.blueprint_to_use"
)}
.blueprints=${this._blueprints}
.value=${this._config.use_blueprint.path}
.disabled=${this.disabled}
@value-changed=${this._blueprintChanged}
></ha-blueprint-picker>
`
: this.hass.localize(
"ui.panel.config.automation.editor.blueprint.no_blueprints"
)
: html`<ha-circular-progress indeterminate></ha-circular-progress>`}
</div>
${this._config.use_blueprint.path
? blueprint && "error" in blueprint
? html`<p class="warning padding">
There is an error in this Blueprint: ${blueprint.error}
</p>`
: html`${blueprint?.metadata.description
? html`<ha-markdown
class="card-content"
breaks
.content=${blueprint.metadata.description}
></ha-markdown>`
: ""}
${blueprint?.metadata?.input &&
Object.keys(blueprint.metadata.input).length
? Object.entries(blueprint.metadata.input).map(
([key, value]) => {
const selector = value?.selector ?? { text: undefined };
const type = Object.keys(selector)[0];
const enhancedSelector = [
"action",
"condition",
"trigger",
].includes(type)
? {
[type]: {
...selector[type],
path: [key],
},
}
: selector;
return html`<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">${value?.name || key}</span>
<ha-markdown
slot="description"
class="card-content"
breaks
.content=${value?.description}
></ha-markdown>
${html`<ha-selector
.hass=${this.hass}
.selector=${enhancedSelector}
.key=${key}
.disabled=${this.disabled}
.required=${value?.default === undefined}
.placeholder=${value?.default}
.value=${this._config.use_blueprint.input &&
key in this._config.use_blueprint.input
? this._config.use_blueprint.input[key]
: value?.default}
@value-changed=${this._inputChanged}
@item-moved=${this._itemMoved}
></ha-selector>`}
</ha-settings-row>`;
}
)
: html`<p class="padding">
${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.no_inputs"
)}
</p>`}`
: ""}
</ha-card>
`;
}
protected abstract _getBlueprints();
private _blueprintChanged(ev) {
ev.stopPropagation();
if (this._config.use_blueprint.path === ev.detail.value) {
return;
}
fireEvent(this, "value-changed", {
value: {
...this._config,
use_blueprint: {
path: ev.detail.value,
},
},
});
}
private _inputChanged(ev) {
ev.stopPropagation();
const target = ev.target as any;
const key = target.key;
const value = ev.detail ? ev.detail.value : target.value;
if (
(this._config.use_blueprint.input &&
this._config.use_blueprint.input[key] === value) ||
(!this._config.use_blueprint.input && value === "")
) {
return;
}
const input = { ...this._config.use_blueprint.input, [key]: value };
fireEvent(this, "value-changed", {
value: {
...this._config,
use_blueprint: {
...this._config.use_blueprint,
input,
},
},
});
}
private _itemMoved(ev) {
ev.stopPropagation();
const { oldIndex, newIndex, oldPath, newPath } = ev.detail;
const input = nestedArrayMove(
this._config.use_blueprint.input,
oldIndex,
newIndex,
oldPath,
newPath
);
fireEvent(this, "value-changed", {
value: {
...this._config,
use_blueprint: {
...this._config.use_blueprint,
input,
},
},
});
}
protected _duplicate() {
fireEvent(this, "duplicate");
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
display: block;
}
ha-card.blueprint {
margin: 0 auto;
}
.padding {
padding: 16px;
}
.link-button-row {
padding: 14px;
}
.blueprint-picker-container {
padding: 0 16px 16px;
}
ha-textfield,
ha-blueprint-picker {
display: block;
}
h3 {
margin: 16px;
}
.introduction {
margin-top: 0;
margin-bottom: 12px;
}
.introduction a {
color: var(--primary-color);
}
p {
margin-bottom: 0;
}
.description {
margin-bottom: 16px;
}
ha-settings-row {
--paper-time-input-justify-content: flex-end;
--settings-row-content-width: 100%;
--settings-row-prefix-display: contents;
border-top: 1px solid var(--divider-color);
}
ha-alert {
margin-bottom: 16px;
display: block;
}
ha-alert.re-order {
border-radius: var(--ha-card-border-radius, 12px);
overflow: hidden;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"blueprint-generic-editor": HaBlueprintGenericEditor;
}
}

View File

@ -1,18 +1,22 @@
import "@material/mwc-button";
import { mdiContentCopy, mdiHelpCircle } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
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-settings-row";
import "../../../../components/ha-switch";
// eslint-disable-next-line
import { formatDate } from "../../../../common/datetime/format_date";
import type { HaSwitch } from "../../../../components/ha-switch";
import {
CloudStatusLoggedIn,
connectCloudRemote,
disconnectCloudRemote,
updateCloudPref,
} from "../../../../data/cloud";
import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast";
@ -29,7 +33,8 @@ export class CloudRemotePref extends LitElement {
return nothing;
}
const { remote_enabled } = this.cloudStatus.prefs;
const { remote_enabled, remote_allow_remote_enable } =
this.cloudStatus.prefs;
const {
remote_connected,
@ -123,16 +128,60 @@ export class CloudRemotePref extends LitElement {
>.
<ha-svg-icon
.url=${`https://${remote_domain}`}
.path=${mdiContentCopy}
@click=${this._copyURL}
.path=${mdiContentCopy}
></ha-svg-icon>
</div>
<div class="card-actions">
<mwc-button @click=${this._openCertInfo}>
${this.hass.localize(
"ui.panel.config.cloud.account.remote.certificate_info"
<ha-expansion-panel
outlined
.header=${this.hass.localize(
"ui.panel.config.cloud.account.remote.advanced_options"
)}
</mwc-button>
>
<ha-settings-row>
<span slot="heading"
>${this.hass.localize(
"ui.panel.config.cloud.account.remote.external_activation"
)}</span
>
<span slot="description"
>${this.hass.localize(
"ui.panel.config.cloud.account.remote.external_activation_secondary"
)}</span
>
<ha-switch
.checked=${remote_allow_remote_enable}
@change=${this._toggleAllowRemoteEnabledChanged}
></ha-switch>
</ha-settings-row>
<ha-settings-row>
<span slot="heading"
>${this.hass.localize(
"ui.panel.config.cloud.account.remote.certificate_info"
)}</span
>
<span slot="description"
>${this.cloudStatus!.remote_certificate
? this.hass.localize(
"ui.panel.config.cloud.account.remote.certificate_expire",
{
date: formatDate(
new Date(
this.cloudStatus.remote_certificate.expire_date
),
this.hass.locale,
this.hass.config
),
}
)
: nothing}</span
>
<ha-button @click=${this._openCertInfo}>
${this.hass.localize(
"ui.panel.config.cloud.account.remote.more_info"
)}
</ha-button>
</ha-settings-row>
</ha-expansion-panel>
</div>
</ha-card>
`;
@ -160,6 +209,20 @@ export class CloudRemotePref extends LitElement {
}
}
private async _toggleAllowRemoteEnabledChanged(ev) {
const toggle = ev.target as HaSwitch;
try {
await updateCloudPref(this.hass, {
remote_allow_remote_enable: toggle.checked,
});
fireEvent(this, "ha-refresh-cloud-status");
} catch (err: any) {
alert(err.message);
toggle.checked = !toggle.checked;
}
}
private async _copyURL(ev): Promise<void> {
const url = ev.currentTarget.url;
await copyToClipboard(url);
@ -204,6 +267,8 @@ export class CloudRemotePref extends LitElement {
position: absolute;
right: 24px;
top: 24px;
inset-inline-end: 24px;
inset-inline-start: initial;
}
.card-actions {
display: flex;
@ -216,6 +281,12 @@ export class CloudRemotePref extends LitElement {
color: var(--secondary-text-color);
cursor: pointer;
}
ha-formfield {
margin-top: 8px;
}
ha-expansion-panel {
margin-top: 8px;
}
`;
}
}

View File

@ -27,7 +27,10 @@ import { addEntitiesToLovelaceView } from "../../../lovelace/editor/add-entities
import type { LovelaceRowConfig } from "../../../lovelace/entity-rows/types";
import { LovelaceRow } from "../../../lovelace/entity-rows/types";
import { EntityRegistryStateEntry } from "../ha-config-device-page";
import { computeCards } from "../../../lovelace/common/generate-lovelace-config";
import {
computeCards,
computeSection,
} from "../../../lovelace/common/generate-lovelace-config";
@customElement("ha-device-entities-card")
export class HaDeviceEntitiesCard extends LitElement {
@ -235,6 +238,9 @@ export class HaDeviceEntitiesCard extends LitElement {
computeCards(this.hass.states, entities, {
title: this.deviceName,
}),
computeSection(entities, {
title: this.deviceName,
}),
entities
);
}

View File

@ -431,19 +431,18 @@ export class HaConfigDevicePage extends LitElement {
<a
href=${ifDefined(
entityState.attributes.id
? `/config/automation/edit/${entityState.attributes.id}`
? `/config/automation/edit/${encodeURIComponent(entityState.attributes.id)}`
: undefined
)}
>
<paper-item
<ha-list-item
hasMeta
.automation=${entityState}
.disabled=${!entityState.attributes.id}
>
<paper-item-body>
${computeStateName(entityState)}
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
${computeStateName(entityState)}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>
${!entityState.attributes.id
? html`
@ -528,15 +527,14 @@ export class HaConfigDevicePage extends LitElement {
: undefined
)}
>
<paper-item
<ha-list-item
hasMeta
.scene=${entityState}
.disabled=${!entityState.attributes.id}
>
<paper-item-body>
${computeStateName(entityState)}
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
${computeStateName(entityState)}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>
${!entityState.attributes.id
? html`
@ -623,12 +621,10 @@ export class HaConfigDevicePage extends LitElement {
return entityState
? html`
<a href=${url}>
<paper-item .script=${script}>
<paper-item-body>
${computeStateName(entityState)}
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
<ha-list-item hasMeta .script=${script}>
${computeStateName(entityState)}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>
`
: "";
@ -1518,11 +1514,6 @@ export class HaConfigDevicePage extends LitElement {
margin-top: 0;
}
paper-item {
cursor: pointer;
font-size: var(--paper-font-body1_-_font-size);
}
a {
text-decoration: none;
color: var(--primary-color);

View File

@ -939,9 +939,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
>
<ha-switch
.checked=${!this._disabledBy && !this._hiddenBy}
.disabled=${this.disabled ||
this._disabledBy ||
(this._hiddenBy && this._hiddenBy !== "user")}
.disabled=${this.disabled || this._disabledBy}
@change=${this._hiddenChanged}
></ha-switch>
</ha-settings-row>

View File

@ -74,7 +74,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
translationKey: "areas",
iconPath: mdiSofa,
iconColor: "#E48629",
components: ["zone"],
component: "zone",
},
{
path: "/hassio",
@ -108,7 +108,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
translationKey: "people",
iconPath: mdiAccount,
iconColor: "#5A87FA",
components: ["person", "users"],
component: ["person", "users"],
},
{
path: "#external-app-configuration",
@ -309,6 +309,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconPath: mdiBackupRestore,
iconColor: "#0D47A1",
component: "backup",
not_component: "hassio",
},
{
path: "/hassio/backups",
@ -341,7 +342,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
translationKey: "hardware",
iconPath: mdiMemory,
iconColor: "#301A8E",
components: ["hassio", "hardware"],
component: ["hassio", "hardware"],
},
],
about: [

View File

@ -171,12 +171,18 @@ class DialogHardwareAvailable extends LitElement implements HassDialog {
ha-icon-button {
position: absolute;
right: 16px;
inset-inline-end: 16px;
inset-inline-start: initial;
top: 10px;
inset-inline-end: 16px;
inset-inline-start: initial;
text-decoration: none;
color: var(--primary-text-color);
}
h2 {
margin: 18px 42px 0 18px;
margin-inline-start: 18px;
margin-inline-end: 42px;
color: var(--primary-text-color);
}
ha-expansion-panel {

View File

@ -177,8 +177,13 @@ class HaInputSelectForm extends LitElement {
const index = (ev.target as any).index;
if (
!(await showConfirmationDialog(this, {
title: "Delete this item?",
text: "Are you sure you want to delete this item?",
title: this.hass.localize(
"ui.dialogs.helper_settings.input_select.confirm_delete.delete"
),
text: this.hass.localize(
"ui.dialogs.helper_settings.input_select.confirm_delete.prompt"
),
destructive: true,
}))
) {
return;

View File

@ -877,6 +877,8 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
color: var(--text-primary-color);
position: absolute;
right: 0px;
inset-inline-end: 0px;
inset-inline-start: initial;
top: 4px;
font-size: 0.65em;
}
@ -884,7 +886,10 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
position: relative;
}
h1 {
margin: 8px 0 0 16px;
margin-top: 8px;
margin-left: 16px;
margin-inline-start: 16px;
margin-inline-end: initial;
}
ha-button-menu {
color: var(--primary-text-color);

View File

@ -90,6 +90,8 @@ export class HaIntegrationActionCard extends LitElement {
position: absolute;
top: 8px;
right: 8px;
inset-inline-end: 8px;
inset-inline-start: initial;
}
.filler {
flex: 1;

View File

@ -20,8 +20,6 @@ import { haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { MatterManageFabricsDialogParams } from "./show-dialog-matter-manage-fabrics";
const NABUCASA_FABRIC = 4939;
@customElement("dialog-matter-manage-fabrics")
class DialogMatterManageFabrics extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -61,8 +59,9 @@ class DialogMatterManageFabrics extends LitElement {
(fabric) =>
html`<ha-list-item
noninteractive
.hasMeta=${this._nodeDiagnostics?.available &&
fabric.vendor_id !== NABUCASA_FABRIC}
.hasMeta=${this._nodeDiagnostics!.available &&
fabric.fabric_index !==
this._nodeDiagnostics!.active_fabric_index}
>${fabric.vendor_name ||
fabric.fabric_label ||
fabric.vendor_id}
@ -99,6 +98,9 @@ class DialogMatterManageFabrics extends LitElement {
private async _removeFabric(ev) {
const fabric: MatterFabricData = ev.target.fabric;
if (this._nodeDiagnostics!.active_fabric_index === fabric.fabric_index) {
return;
}
const fabricName =
fabric.vendor_name || fabric.fabric_label || fabric.vendor_id.toString();
const confirm = await showConfirmationDialog(this, {

View File

@ -137,6 +137,7 @@ class DialogMatterPingNode extends LitElement {
public closeDialog(): void {
this.device_id = undefined;
this._status = undefined;
this._pingResult = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}

View File

@ -54,6 +54,7 @@ import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { brandsUrl } from "../../../../../util/brands-url";
import { fileDownload } from "../../../../../util/file_download";
import { documentationUrl } from "../../../../../util/documentation-url";
interface ThreadNetwork {
name: string;
@ -123,11 +124,16 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
)}
</h3>
<ha-svg-icon .path=${mdiDevices}></ha-svg-icon>
<mwc-button @click=${this._addOTBR}
>${this.hass.localize(
"ui.panel.config.thread.add_open_thread_border_router"
)}</mwc-button
<a
href=${documentationUrl(this.hass, `/integrations/thread`)}
target="_blank"
>
<mwc-button
>${this.hass.localize(
"ui.panel.config.thread.more_info"
)}</mwc-button
>
</a>
</div>
</ha-card>`}
${networks.networks.length

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