Compare commits

..

1 Commits

Author SHA1 Message Date
Bram Kragten
1a2daf8a7a 20240529.0 (#20901) 2024-05-29 18:27:40 +02:00
132 changed files with 3087 additions and 4449 deletions

View File

@@ -126,5 +126,6 @@
"lit-a11y/anchor-is-valid": "warn",
"lit-a11y/role-has-required-aria-attrs": "warn"
},
"plugins": ["unused-imports"]
"plugins": ["disable", "unused-imports"],
"processor": "disable/disable"
}

View File

@@ -21,7 +21,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.1.6
with:
ref: dev
@@ -57,7 +57,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.1.6
with:
ref: master

View File

@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.1.6
- name: Setup Node
uses: actions/setup-node@v4.0.2
with:
@@ -58,7 +58,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.1.6
- name: Setup Node
uses: actions/setup-node@v4.0.2
with:
@@ -76,7 +76,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.1.6
- name: Setup Node
uses: actions/setup-node@v4.0.2
with:
@@ -100,7 +100,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.1.6
- name: Setup Node
uses: actions/setup-node@v4.0.2
with:

View File

@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.1.6
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.

View File

@@ -22,7 +22,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.1.6
with:
ref: dev
@@ -58,7 +58,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.1.6
with:
ref: master

View File

@@ -16,7 +16,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.1.6
- name: Setup Node
uses: actions/setup-node@v4.0.2

View File

@@ -21,7 +21,7 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.1.6
- name: Setup Node
uses: actions/setup-node@v4.0.2

View File

@@ -20,7 +20,7 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.1.6
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@v2.1.11
uses: relative-ci/agent-action@v2.1.10
with:
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }}

View File

@@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.1.6
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master
@@ -55,7 +55,7 @@ jobs:
script/release
- name: Upload release assets
uses: softprops/action-gh-release@v2.0.6
uses: softprops/action-gh-release@v2.0.5
with:
files: |
dist/*.whl

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.1.6
- name: Upload Translations
run: |

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.3.1.cjs
yarnPath: .yarn/releases/yarn-4.2.2.cjs

View File

@@ -92,8 +92,8 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
[
"@babel/preset-env",
{
useBuiltIns: "usage",
corejs: dependencies["core-js"],
useBuiltIns: latestBuild ? false : "usage",
corejs: latestBuild ? false : dependencies["core-js"],
bugfixes: true,
shippedProposals: true,
},
@@ -157,7 +157,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
exclude: [
path.join(paths.polymer_dir, "src/resources/polyfills"),
...[
"@formatjs/(?:ecma402-abstract|intl-\\w+)",
"@formatjs/intl-\\w+",
"@lit-labs/virtualizer/polyfills",
"@webcomponents/scoped-custom-element-registry",
"element-internals-polyfill",

View File

@@ -1,6 +1,7 @@
import "@material/mwc-button/mwc-button";
import { ActionDetail } from "@material/mwc-list/mwc-list";
import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-listbox/paper-listbox";
import { Auth, Connection } from "home-assistant-js-websocket";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -27,7 +28,6 @@ import { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view";
import "../../../../src/layouts/hass-loading-screen";
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
import "./hc-layout";
import "../../../../src/components/ha-list-item";
@customElement("hc-cast")
class HcCast extends LitElement {
@@ -83,37 +83,37 @@ class HcCast extends LitElement {
`
: html`
<div class="section-header">PICK A VIEW</div>
<mwc-list @action=${this._handlePickView} activatable>
<paper-listbox
attr-for-selected="data-path"
.selected=${this.castManager.status.lovelacePath || ""}
>
${(
this.lovelaceViews ?? [
generateDefaultViewConfig({}, {}, {}, {}, () => ""),
]
).map(
(view, idx) =>
html`<ha-list-item
graphic="avatar"
.activated=${this.castManager.status?.lovelacePath ===
(view.path ?? idx)}
.selected=${this.castManager.status?.lovelacePath ===
(view.path ?? idx)}
(view, idx) => html`
<paper-icon-item
@click=${this._handlePickView}
data-path=${view.path || idx}
>
${view.title || view.path || "Unnamed view"}
${view.icon
? html`
<ha-icon
.icon=${view.icon}
slot="graphic"
slot="item-icon"
></ha-icon>
`
: html`<ha-svg-icon
slot="item-icon"
.path=${mdiViewDashboard}
></ha-svg-icon>`}</ha-list-item
> `
)}</mwc-list
>
></ha-svg-icon>`}
${view.title || view.path || "Unnamed view"}
</paper-icon-item>
`
)}
</paper-listbox>
`}
<div class="card-actions">
${this.castManager.status
? html`
@@ -185,8 +185,8 @@ class HcCast extends LitElement {
this.castManager.requestSession();
}
private async _handlePickView(ev: CustomEvent<ActionDetail>) {
const path = this.lovelaceViews![ev.detail.index].path ?? ev.detail.index;
private async _handlePickView(ev: Event) {
const path = (ev.currentTarget as any).getAttribute("data-path");
await ensureConnectedCastSession(this.castManager!, this.auth!);
castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path);
}
@@ -249,14 +249,26 @@ class HcCast extends LitElement {
height: 18px;
}
ha-list-item ha-icon,
ha-list-item ha-svg-icon {
paper-listbox {
padding-top: 0;
}
paper-listbox ha-icon,
paper-listbox ha-svg-icon {
padding: 12px;
color: var(--secondary-text-color);
}
:host([hide-icons]) ha-icon {
display: none;
paper-icon-item {
cursor: pointer;
}
paper-icon-item[disabled] {
cursor: initial;
}
:host([hide-icons]) paper-icon-item {
--paper-item-icon-width: 0px;
}
.spacer {

View File

@@ -1,9 +1,8 @@
import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-card";
import "../../../src/components/ha-button";
import "../../../src/components/ha-circular-progress";
import { LovelaceCardConfig } from "../../../src/data/lovelace/config/card";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
@@ -12,6 +11,7 @@ import {
demoConfigs,
selectedDemoConfig,
selectedDemoConfigIndex,
setDemoConfig,
} from "../configs/demo-configs";
@customElement("ha-demo-card")
@@ -64,9 +64,9 @@ export class HADemoCard extends LitElement implements LovelaceCard {
)}
</div>
<ha-button @click=${this._nextConfig} .disabled=${this._switching}>
<mwc-button @click=${this._nextConfig} .disabled=${this._switching}>
${this.hass.localize("ui.panel.page-demo.cards.demo.next_demo")}
</ha-button>
</mwc-button>
</div>
<div class="content">
<p class="small-hidden">
@@ -87,9 +87,9 @@ export class HADemoCard extends LitElement implements LovelaceCard {
</div>
<div class="actions small-hidden">
<a href="https://www.home-assistant.io" target="_blank">
<ha-button>
<mwc-button>
${this.hass.localize("ui.panel.page-demo.cards.demo.learn_more")}
</ha-button>
</mwc-button>
</a>
</div>
</ha-card>
@@ -113,7 +113,13 @@ export class HADemoCard extends LitElement implements LovelaceCard {
private async _updateConfig(index: number) {
this._switching = true;
fireEvent(this, "set-demo-config" as any, { index });
try {
await setDemoConfig(this.hass, this.lovelace!, index);
} catch (err: any) {
alert("Failed to switch config :-(");
} finally {
this._switching = false;
}
}
static get styles(): CSSResultGroup {
@@ -143,7 +149,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
height: 60px;
}
.picker ha-button {
.picker mwc-button {
margin-right: 8px;
}

View File

@@ -1,12 +1,9 @@
import type { LocalizeFunc } from "../../../src/common/translations/localize";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import {
selectedDemoConfig,
selectedDemoConfigIndex,
setDemoConfig,
} from "../configs/demo-configs";
import { selectedDemoConfig } from "../configs/demo-configs";
import "../custom-cards/cast-demo-row";
import "../custom-cards/ha-demo-card";
import type { HADemoCard } from "../custom-cards/ha-demo-card";
export const mockLovelace = (
hass: MockHomeAssistant,
@@ -22,22 +19,17 @@ export const mockLovelace = (
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
};
customElements.whenDefined("hui-root").then(() => {
customElements.whenDefined("hui-view").then(() => {
// eslint-disable-next-line
const HUIRoot = customElements.get("hui-root")!;
const HUIView = customElements.get("hui-view");
// Patch HUI-VIEW to make the lovelace object available to the demo card
const oldCreateCard = HUIView!.prototype.createCardElement;
const oldFirstUpdated = HUIRoot.prototype.firstUpdated;
HUIRoot.prototype.firstUpdated = function (changedProperties) {
oldFirstUpdated.call(this, changedProperties);
this.addEventListener("set-demo-config", async (ev) => {
const index = (ev as CustomEvent).detail.index;
try {
await setDemoConfig(this.hass, this.lovelace!, index);
} catch (err: any) {
setDemoConfig(this.hass, this.lovelace!, selectedDemoConfigIndex);
alert("Failed to switch config :-(");
}
});
HUIView!.prototype.createCardElement = function (config) {
const el = oldCreateCard.call(this, config);
if (el.tagName === "HA-DEMO-CARD") {
(el as HADemoCard).lovelace = this.lovelace;
}
return el;
};
});

View File

@@ -1,9 +1,7 @@
import { load } from "js-yaml";
import { LitElement, PropertyValueMap, css, html, nothing } from "lit";
import { html, css, LitElement, PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import "../../../src/panels/lovelace/cards/hui-card";
import type { HuiCard } from "../../../src/panels/lovelace/cards/hui-card";
import { createCardElement } from "../../../src/panels/lovelace/create-element/create-card-element";
import { HomeAssistant } from "../../../src/types";
export interface DemoCardConfig {
@@ -21,12 +19,7 @@ class DemoCard extends LitElement {
@state() private _size?: number;
@query("hui-card", false) private _card?: HuiCard;
private _config = memoizeOne((config: string) => {
const c = (load(config) as any)[0];
return c;
});
@query("#card") private _card!: HTMLElement;
render() {
return html`
@@ -37,32 +30,63 @@ class DemoCard extends LitElement {
: ""}
</h2>
<div class="root">
<hui-card
.config=${this._config(this.config.config)}
.hass=${this.hass}
@card-updated=${this._cardUpdated}
></hui-card>
${this.showConfig
? html`<pre>${this.config.config.trim()}</pre>`
: nothing}
<div id="card"></div>
${this.showConfig ? html`<pre>${this.config.config.trim()}</pre>` : ""}
</div>
`;
}
private async _cardUpdated(ev) {
ev.stopPropagation();
this._updateSize();
updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("config")) {
const card = this._card;
while (card.lastChild) {
card.removeChild(card.lastChild);
}
const el = this._createCardElement((load(this.config.config) as any)[0]);
card.appendChild(el);
this._getSize(el);
}
if (changedProps.has("hass")) {
const card = this._card.lastChild;
if (card) {
(card as any).hass = this.hass;
}
}
}
private async _updateSize() {
this._size = await this._card?.getCardSize();
async _getSize(el) {
await customElements.whenDefined(el.localName);
if (!("getCardSize" in el)) {
this._size = undefined;
return;
}
this._size = await el.getCardSize();
}
protected update(
_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
): void {
super.update(_changedProperties);
this._updateSize();
_createCardElement(cardConfig) {
const element = createCardElement(cardConfig);
if (this.hass) {
element.hass = this.hass;
}
element.addEventListener(
"ll-rebuild",
(ev) => {
ev.stopPropagation();
this._rebuildCard(element, cardConfig);
},
{ once: true }
);
return element;
}
_rebuildCard(cardElToReplace, config) {
const newCardEl = this._createCardElement(config);
cardElToReplace.parentElement.replaceChild(newCardEl, cardElToReplace);
}
static styles = css`
@@ -77,7 +101,7 @@ class DemoCard extends LitElement {
font-size: 0.5em;
color: var(--primary-text-color);
}
hui-card {
#card {
max-width: 400px;
width: 100vw;
}

View File

@@ -25,15 +25,15 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.24.7",
"@braintree/sanitize-url": "7.0.3",
"@codemirror/autocomplete": "6.16.3",
"@codemirror/commands": "6.6.0",
"@codemirror/language": "6.10.2",
"@babel/runtime": "7.24.6",
"@braintree/sanitize-url": "7.0.2",
"@codemirror/autocomplete": "6.16.0",
"@codemirror/commands": "6.5.0",
"@codemirror/language": "6.10.1",
"@codemirror/legacy-modes": "6.4.0",
"@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.28.2",
"@codemirror/view": "6.26.3",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.12.5",
"@formatjs/intl-displaynames": "6.6.8",
@@ -43,17 +43,17 @@
"@formatjs/intl-numberformat": "8.10.3",
"@formatjs/intl-pluralrules": "5.2.14",
"@formatjs/intl-relativetimeformat": "11.2.14",
"@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",
"@fullcalendar/core": "6.1.13",
"@fullcalendar/daygrid": "6.1.13",
"@fullcalendar/interaction": "6.1.13",
"@fullcalendar/list": "6.1.13",
"@fullcalendar/luxon3": "6.1.13",
"@fullcalendar/timegrid": "6.1.13",
"@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.13",
"@lit-labs/virtualizer": "2.0.12",
"@lrnwebcomponents/simple-tooltip": "8.0.2",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
@@ -88,8 +88,8 @@
"@polymer/paper-tabs": "3.1.0",
"@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.4.0",
"@vaadin/vaadin-themable-mixin": "24.4.0",
"@vaadin/combo-box": "24.3.13",
"@vaadin/vaadin-themable-mixin": "24.3.13",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -110,7 +110,7 @@
"fuse.js": "7.0.0",
"google-timezones-json": "1.2.0",
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.4.0",
"home-assistant-js-websocket": "9.3.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.5.14",
"js-yaml": "4.1.0",
@@ -133,7 +133,7 @@
"tinykeys": "2.1.0",
"tsparticles-engine": "2.12.0",
"tsparticles-preset-links": "2.12.0",
"ua-parser-js": "1.0.38",
"ua-parser-js": "1.0.37",
"unfetch": "5.0.0",
"vis-data": "7.1.9",
"vis-network": "9.1.9",
@@ -149,26 +149,26 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.24.7",
"@babel/core": "7.24.6",
"@babel/helper-define-polyfill-provider": "0.6.2",
"@babel/plugin-proposal-decorators": "7.24.7",
"@babel/plugin-transform-runtime": "7.24.7",
"@babel/preset-env": "7.24.7",
"@babel/preset-typescript": "7.24.7",
"@babel/plugin-proposal-decorators": "7.24.6",
"@babel/plugin-transform-runtime": "7.24.6",
"@babel/preset-env": "7.24.6",
"@babel/preset-typescript": "7.24.6",
"@bundle-stats/plugin-webpack-filter": "4.13.2",
"@koa/cors": "5.0.0",
"@lokalise/node-api": "12.5.0",
"@octokit/auth-oauth-device": "7.1.1",
"@octokit/plugin-retry": "7.1.1",
"@octokit/rest": "21.0.0",
"@octokit/rest": "20.1.1",
"@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.4",
"@rollup/plugin-commonjs": "26.0.1",
"@rollup/plugin-commonjs": "25.0.8",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-node-resolve": "15.2.3",
"@rollup/plugin-replace": "5.0.7",
"@rollup/plugin-replace": "5.0.5",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.15",
"@types/chromecast-caf-receiver": "6.0.14",
"@types/chromecast-caf-sender": "1.0.10",
"@types/color-name": "1.1.4",
"@types/glob": "8.1.0",
@@ -185,8 +185,8 @@
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "7.13.1",
"@typescript-eslint/parser": "7.13.1",
"@typescript-eslint/eslint-plugin": "7.10.0",
"@typescript-eslint/parser": "7.10.0",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3",
@@ -198,14 +198,15 @@
"eslint-config-airbnb-typescript": "18.0.0",
"eslint-config-prettier": "9.1.0",
"eslint-import-resolver-webpack": "0.13.8",
"eslint-plugin-disable": "2.0.3",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-lit": "1.14.0",
"eslint-plugin-lit": "1.13.0",
"eslint-plugin-lit-a11y": "4.1.2",
"eslint-plugin-unused-imports": "4.0.0",
"eslint-plugin-wc": "2.1.0",
"fancy-log": "2.0.0",
"fs-extra": "11.2.0",
"glob": "10.4.2",
"glob": "10.4.1",
"gulp": "5.0.0",
"gulp-json-transform": "0.5.0",
"gulp-rename": "2.0.0",
@@ -214,7 +215,7 @@
"husky": "9.0.11",
"instant-mocha": "1.5.2",
"jszip": "3.10.1",
"lint-staged": "15.2.7",
"lint-staged": "15.2.5",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
@@ -224,7 +225,7 @@
"object-hash": "3.0.0",
"open": "10.1.0",
"pinst": "3.0.0",
"prettier": "3.3.2",
"prettier": "3.2.5",
"rollup": "2.79.1",
"rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2",
@@ -233,18 +234,18 @@
"sinon": "18.0.0",
"source-map-url": "0.4.1",
"systemjs": "6.15.1",
"tar": "7.4.0",
"tar": "7.1.0",
"terser-webpack-plugin": "5.3.10",
"transform-async-modules-webpack-plugin": "1.1.1",
"ts-lit-plugin": "2.0.2",
"typescript": "5.4.5",
"webpack": "5.92.1",
"webpack": "5.91.0",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.0.4",
"webpack-manifest-plugin": "5.0.0",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "6.0.1",
"workbox-build": "7.1.1"
"workbox-build": "7.1.0"
},
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
"resolutions": {
@@ -253,9 +254,8 @@
"lit": "2.8.0",
"clean-css": "5.3.3",
"@lit/reactive-element": "1.6.3",
"@fullcalendar/daygrid": "6.1.11",
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch",
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
},
"packageManager": "yarn@4.3.1"
"packageManager": "yarn@4.2.2"
}

View File

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

View File

@@ -31,7 +31,6 @@ import {
mdiFormatListBulleted,
mdiFormatListCheckbox,
mdiFormTextbox,
mdiForumOutline,
mdiGauge,
mdiGoogleAssistant,
mdiGoogleCirclesCommunities,
@@ -99,7 +98,7 @@ export const FIXED_DOMAIN_ICONS = {
calendar: mdiCalendar,
climate: mdiThermostat,
configurator: mdiCog,
conversation: mdiForumOutline,
conversation: mdiMicrophoneMessage,
counter: mdiCounter,
date: mdiCalendar,
datetime: mdiCalendarClock,
@@ -236,8 +235,6 @@ export const SENSOR_ENTITIES = [
"weather",
];
export const ASSIST_ENTITIES = ["conversation", "stt", "tts"];
/** Domains that render an input element instead of a text value when displayed in a row.
* Those rows should then not show a cursor pointer when hovered (which would normally
* be the default) unless the element itself enforces it (e.g. a button). Also those elements

View File

@@ -1 +0,0 @@
export const preventDefault = (ev) => ev.preventDefault();

View File

@@ -47,8 +47,6 @@ export class HaCodeEditor extends ReactiveElement {
@property({ type: Boolean }) public readOnly = false;
@property({ type: Boolean }) public linewrap = false;
@property({ type: Boolean, attribute: "autocomplete-entities" })
public autocompleteEntities = false;
@@ -136,13 +134,6 @@ export class HaCodeEditor extends ReactiveElement {
),
});
}
if (changedProps.has("linewrap")) {
transactions.push({
effects: this._loadedCodeMirror!.linewrapCompartment!.reconfigure(
this.linewrap ? this._loadedCodeMirror!.EditorView.lineWrapping : []
),
});
}
if (changedProps.has("_value") && this._value !== this.value) {
transactions.push({
changes: {
@@ -190,9 +181,6 @@ export class HaCodeEditor extends ReactiveElement {
this._loadedCodeMirror.readonlyCompartment.of(
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
),
this._loadedCodeMirror.linewrapCompartment.of(
this.linewrap ? this._loadedCodeMirror.EditorView.lineWrapping : []
),
this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate),
];

View File

@@ -89,18 +89,13 @@ export class HaFilterDomains extends LitElement {
});
return Array.from(domains.values())
.map((domain) => ({
domain,
name: domainToName(this.hass.localize, domain),
}))
.filter(
(entry) =>
!filter ||
entry.domain.toLowerCase().includes(filter) ||
entry.name.toLowerCase().includes(filter)
entry.toLowerCase().includes(filter) ||
domainToName(this.hass.localize, entry).toLowerCase().includes(filter)
)
.sort((a, b) => stringCompare(a.name, b.name, this.hass.locale.language))
.map((entry) => entry.domain);
.sort((a, b) => stringCompare(a, b, this.hass.locale.language));
});
protected updated(changed) {

View File

@@ -2,7 +2,7 @@ import type { Selector } from "../../data/selector";
import type { HaFormSchema } from "./types";
export const computeInitialHaFormData = (
schema: HaFormSchema[] | readonly HaFormSchema[]
schema: HaFormSchema[]
): Record<string, any> => {
const data = {};
schema.forEach((field) => {
@@ -36,8 +36,6 @@ export const computeInitialHaFormData = (
minutes: 0,
seconds: 0,
};
} else if (field.type === "expandable") {
data[field.name] = computeInitialHaFormData(field.schema);
} else if ("selector" in field) {
const selector: Selector = field.selector;

View File

@@ -1,233 +0,0 @@
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "./ha-icon-button";
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
import { mdiRestore } from "@mdi/js";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types";
type GridSizeValue = {
rows?: number;
columns?: number;
};
@customElement("ha-grid-size-picker")
export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: GridSizeValue;
@property({ attribute: false }) public rows = 6;
@property({ attribute: false }) public columns = 4;
@property({ attribute: false }) public rowMin?: number;
@property({ attribute: false }) public rowMax?: number;
@property({ attribute: false }) public columnMin?: number;
@property({ attribute: false }) public columnMax?: number;
@property({ attribute: false }) public isDefault?: boolean;
@state() public _localValue?: GridSizeValue = undefined;
protected willUpdate(changedProperties) {
if (changedProperties.has("value")) {
this._localValue = this.value;
}
}
protected render() {
return html`
<div class="grid">
<ha-grid-layout-slider
aria-label=${this.hass.localize(
"ui.components.grid-size-picker.columns"
)}
id="columns"
.min=${this.columnMin ?? 1}
.max=${this.columnMax ?? this.columns}
.range=${this.columns}
.value=${this.value?.columns}
@value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved}
></ha-grid-layout-slider>
<ha-grid-layout-slider
aria-label=${this.hass.localize(
"ui.components.grid-size-picker.rows"
)}
id="rows"
.min=${this.rowMin ?? 1}
.max=${this.rowMax ?? this.rows}
.range=${this.rows}
vertical
.value=${this.value?.rows}
@value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved}
></ha-grid-layout-slider>
${!this.isDefault
? html`
<ha-icon-button
@click=${this._reset}
class="reset"
.path=${mdiRestore}
label=${this.hass.localize(
"ui.components.grid-size-picker.reset_default"
)}
title=${this.hass.localize(
"ui.components.grid-size-picker.reset_default"
)}
>
</ha-icon-button>
`
: nothing}
<div
class="preview"
style=${styleMap({
"--total-rows": this.rows,
"--total-columns": this.columns,
"--rows": this._localValue?.rows,
"--columns": this._localValue?.columns,
})}
>
<div>
${Array(this.rows * this.columns)
.fill(0)
.map((_, index) => {
const row = Math.floor(index / this.columns) + 1;
const column = (index % this.columns) + 1;
const disabled =
(this.rowMin !== undefined && row < this.rowMin) ||
(this.rowMax !== undefined && row > this.rowMax) ||
(this.columnMin !== undefined && column < this.columnMin) ||
(this.columnMax !== undefined && column > this.columnMax);
return html`
<div
class="cell"
data-row=${row}
data-column=${column}
?disabled=${disabled}
@click=${this._cellClick}
></div>
`;
})}
</div>
<div class="selected">
<div class="cell"></div>
</div>
</div>
</div>
`;
}
_cellClick(ev) {
const cell = ev.currentTarget as HTMLElement;
if (cell.getAttribute("disabled") !== null) return;
const rows = Number(cell.getAttribute("data-row"));
const columns = Number(cell.getAttribute("data-column"));
fireEvent(this, "value-changed", {
value: { rows, columns },
});
}
private _valueChanged(ev) {
ev.stopPropagation();
const key = ev.currentTarget.id;
const newValue = {
...this.value,
[key]: ev.detail.value,
};
fireEvent(this, "value-changed", { value: newValue });
}
private _reset(ev) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
rows: undefined,
columns: undefined,
},
});
}
private _sliderMoved(ev) {
ev.stopPropagation();
const key = ev.currentTarget.id;
const value = ev.detail.value;
if (value === undefined) return;
this._localValue = {
...this.value,
[key]: ev.detail.value,
};
}
static styles = [
css`
.grid {
display: grid;
grid-template-areas:
"reset column-slider"
"row-slider preview";
grid-template-rows: auto 1fr;
grid-template-columns: auto 1fr;
gap: 8px;
}
#columns {
grid-area: column-slider;
}
#rows {
grid-area: row-slider;
}
.reset {
grid-area: reset;
}
.preview {
position: relative;
grid-area: preview;
aspect-ratio: 1 / 1;
}
.preview > div {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: grid;
grid-template-columns: repeat(var(--total-columns), 1fr);
grid-template-rows: repeat(var(--total-rows), 1fr);
gap: 4px;
}
.preview .cell {
background-color: var(--disabled-color);
grid-column: span 1;
grid-row: span 1;
border-radius: 4px;
opacity: 0.2;
cursor: pointer;
}
.preview .cell[disabled] {
opacity: 0.05;
cursor: initial;
}
.selected {
pointer-events: none;
}
.selected .cell {
background-color: var(--primary-color);
grid-column: 1 / span var(--columns, 0);
grid-row: 1 / span var(--rows, 0);
opacity: 0.5;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-grid-size-picker": HaGridSizeEditor;
}
}

View File

@@ -198,7 +198,8 @@ export class HaIconPicker extends LitElement {
static get styles() {
return css`
*[slot="icon"] {
ha-icon,
ha-svg-icon {
color: var(--primary-text-color);
position: relative;
bottom: 2px;

View File

@@ -64,12 +64,6 @@ const SELECTOR_SCHEMAS = {
selector: { boolean: {} },
},
] as const,
floor: [
{
name: "multiple",
selector: { boolean: {} },
},
] as const,
icon: [] as const,
location: [] as const,
media: [] as const,

View File

@@ -32,7 +32,6 @@ export class HaTemplateSelector extends LitElement {
autocomplete-icons
@value-changed=${this._handleChange}
dir="ltr"
linewrap
></ha-code-editor>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`

View File

@@ -327,7 +327,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
for (const entityId of Object.keys(this.hass.states)) {
if (
entityId.startsWith("update.") &&
!this.hass.entities[entityId]?.hidden &&
updateCanInstall(this.hass.states[entityId] as UpdateEntity)
) {
updateCount++;

View File

@@ -48,7 +48,7 @@ class HaEntityMarker extends LitElement {
width: 48px;
height: 48px;
font-size: var(--ha-marker-font-size, 1.5em);
border-radius: var(--ha-marker-border-radius, 50%);
border-radius: 50%;
border: 1px solid var(--ha-marker-color, var(--primary-color));
color: var(--primary-text-color);
background-color: var(--card-background-color);

View File

@@ -138,17 +138,6 @@ export const adminChangePassword = (
password,
});
export const adminChangeUsername = (
hass: HomeAssistant,
userId: string,
username: string
) =>
hass.callWS<void>({
type: "config/auth_provider/homeassistant/admin_change_username",
user_id: userId,
username,
});
export const deleteAllRefreshTokens = (
hass: HomeAssistant,
token_type?: RefreshTokenType,

View File

@@ -901,7 +901,7 @@ const tryDescribeCondition = (
)
: undefined;
if (condition.above !== undefined && condition.below !== undefined) {
if (condition.above && condition.below) {
return hass.localize(
`${conditionsTranslationBaseKey}.numeric_state.description.above-below`,
{
@@ -912,7 +912,7 @@ const tryDescribeCondition = (
}
);
}
if (condition.above !== undefined) {
if (condition.above) {
return hass.localize(
`${conditionsTranslationBaseKey}.numeric_state.description.above`,
{
@@ -922,7 +922,7 @@ const tryDescribeCondition = (
}
);
}
if (condition.below !== undefined) {
if (condition.below) {
return hass.localize(
`${conditionsTranslationBaseKey}.numeric_state.description.below`,
{

View File

@@ -6,7 +6,6 @@ export interface ConfigUpdateValues {
latitude: number;
longitude: number;
elevation: number;
radius: number;
unit_system: "metric" | "us_customary";
time_zone: string;
external_url?: string | null;

View File

@@ -249,22 +249,6 @@ export const localizeDeviceAutomationTrigger = (
) ||
(trigger.subtype ? `"${trigger.subtype}" ${trigger.type}` : trigger.type!);
export const localizeExtraFieldsComputeLabelCallback =
(hass: HomeAssistant, deviceAutomation: DeviceAutomation) =>
// Returns a callback for ha-form to calculate labels per schema object
(schema): string =>
hass.localize(
`component.${deviceAutomation.domain}.device_automation.extra_fields.${schema.name}`
) || schema.name;
export const localizeExtraFieldsComputeHelperCallback =
(hass: HomeAssistant, deviceAutomation: DeviceAutomation) =>
// Returns a callback for ha-form to calculate helper texts per schema object
(schema): string | undefined =>
hass.localize(
`component.${deviceAutomation.domain}.device_automation.extra_fields_descriptions.${schema.name}`
);
export const sortDeviceAutomations = (
automationA: DeviceAutomation,
automationB: DeviceAutomation

View File

@@ -30,7 +30,6 @@ export interface LovelaceViewElement extends HTMLElement {
export interface LovelaceSectionElement extends HTMLElement {
hass?: HomeAssistant;
lovelace?: Lovelace;
preview?: boolean;
viewIndex?: number;
index?: number;
cards?: HuiCard[];

View File

@@ -2,7 +2,6 @@ import { HomeAssistant } from "../types";
export interface OTBRInfo {
active_dataset_tlvs: string;
border_agent_id: string;
channel: number;
extended_address: string;
url: string;

View File

@@ -12,6 +12,14 @@ export interface RecorderInfo {
thread_running: boolean;
}
export interface RecordedEntities {
entity_ids: string[];
}
export interface RecordedExcludedEntities {
recorded_ids: string[];
excluded_ids: string[];
}
export type StatisticType = "change" | "state" | "sum" | "min" | "max" | "mean";
export interface Statistics {
@@ -324,3 +332,25 @@ export const getDisplayUnit = (
export const isExternalStatistic = (statisticsId: string): boolean =>
statisticsId.includes(":");
let recordedExcludedEntitiesCache: RecordedExcludedEntities | undefined;
export const getRecordedExcludedEntities = async (
hass: HomeAssistant
): Promise<RecordedExcludedEntities> => {
if (recordedExcludedEntitiesCache) {
return recordedExcludedEntitiesCache;
}
const recordedEntities = await hass.callWS<RecordedEntities>({
type: "recorder/recorded_entities",
});
recordedExcludedEntitiesCache = {
recorded_ids: recordedEntities.entity_ids,
excluded_ids: Object.keys(hass.states).filter(
(id) => !recordedEntities.entity_ids.includes(id)
),
};
return recordedExcludedEntitiesCache;
};

View File

@@ -1,21 +0,0 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
export interface ThresholdPreview {
state: string;
attributes: Record<string, any>;
}
export const subscribePreviewThreshold = (
hass: HomeAssistant,
flow_id: string,
flow_type: "config_flow" | "options_flow",
user_input: Record<string, any>,
callback: (preview: ThresholdPreview) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(callback, {
type: "threshold/start_preview",
flow_id,
flow_type,
user_input,
});

View File

@@ -144,7 +144,7 @@ export const checkForEntityUpdates = async (
// there is no reliable way to know if all the updates are done updating, so we just wait a bit for now...
await new Promise((r) => {
setTimeout(r, 15000);
setTimeout(r, 10000);
});
unsubscribeEvents();

View File

@@ -1,9 +1,6 @@
import {
mdiAlertCircleOutline,
mdiGauge,
mdiThermometer,
mdiThermometerWater,
mdiSunWireless,
mdiWaterPercent,
mdiWeatherCloudy,
mdiWeatherFog,
@@ -117,15 +114,10 @@ export const weatherIcons = {
};
export const weatherAttrIcons = {
apparent_temperature: mdiThermometer,
cloud_coverage: mdiWeatherCloudy,
dew_point: mdiThermometerWater,
humidity: mdiWaterPercent,
wind_bearing: mdiWeatherWindy,
wind_speed: mdiWeatherWindy,
pressure: mdiGauge,
temperature: mdiThermometer,
uv_index: mdiSunWireless,
visibility: mdiWeatherFog,
precipitation: mdiWeatherRainy,
};
@@ -229,8 +221,6 @@ export const getWeatherUnit = (
stateObj.attributes.pressure_unit ||
(lengthUnit === "km" ? "hPa" : "inHg")
);
case "apparent_temperature":
case "dew_point":
case "temperature":
case "templow":
return (
@@ -238,7 +228,6 @@ export const getWeatherUnit = (
);
case "wind_speed":
return stateObj.attributes.wind_speed_unit || `${lengthUnit}/h`;
case "cloud_coverage":
case "humidity":
case "precipitation_probability":
return "%";

View File

@@ -14,7 +14,6 @@ export interface Zone {
export interface HomeZoneMutableParams {
latitude: number;
longitude: number;
radius: number;
}
export interface ZoneMutableParams {

View File

@@ -487,14 +487,14 @@ export const stopZwaveExclusion = (hass: HomeAssistant, entry_id: string) =>
export const zwaveGrantSecurityClasses = (
hass: HomeAssistant,
entry_id: string,
securityClasses: SecurityClass[],
clientSideAuth?: boolean
security_classes: SecurityClass[],
client_side_auth?: boolean
) =>
hass.callWS({
type: "zwave_js/grant_security_classes",
entry_id,
securityClasses,
clientSideAuth,
security_classes,
client_side_auth,
});
export const zwaveTryParseDskFromQrCode = (

View File

@@ -1,108 +0,0 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { FlowType } from "../../../data/data_entry_flow";
import {
ThresholdPreview,
subscribePreviewThreshold,
} from "../../../data/threshold";
import { HomeAssistant } from "../../../types";
import "./entity-preview-row";
import { debounce } from "../../../common/util/debounce";
import { fireEvent } from "../../../common/dom/fire_event";
@customElement("flow-preview-threshold")
class FlowPreviewThreshold extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public flowType!: FlowType;
public handler!: string;
@property() public stepId!: string;
@property() public flowId!: string;
@property() public stepData!: Record<string, any>;
@state() private _preview?: HassEntity;
@state() private _error?: string;
private _unsub?: Promise<UnsubscribeFunc>;
disconnectedCallback(): void {
super.disconnectedCallback();
if (this._unsub) {
this._unsub.then((unsub) => unsub());
this._unsub = undefined;
}
}
willUpdate(changedProps) {
if (changedProps.has("stepData")) {
this._debouncedSubscribePreview();
}
}
protected render() {
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
}
return html`<entity-preview-row
.hass=${this.hass}
.stateObj=${this._preview}
></entity-preview-row>`;
}
private _setPreview = (preview: ThresholdPreview) => {
const now = new Date().toISOString();
this._preview = {
entity_id: `${this.stepId}.___flow_preview___`,
last_changed: now,
last_updated: now,
context: { id: "", parent_id: null, user_id: null },
...preview,
};
};
private _debouncedSubscribePreview = debounce(() => {
this._subscribePreview();
}, 250);
private async _subscribePreview() {
if (this._unsub) {
(await this._unsub)();
this._unsub = undefined;
}
if (this.flowType === "repair_flow") {
return;
}
try {
this._unsub = subscribePreviewThreshold(
this.hass,
this.flowId,
this.flowType,
this.stepData,
this._setPreview
);
await this._unsub;
fireEvent(this, "set-flow-errors", { errors: {} });
} catch (err: any) {
if (typeof err.message === "string") {
this._error = err.message;
} else {
this._error = undefined;
fireEvent(this, "set-flow-errors", err.message);
}
this._unsub = undefined;
this._preview = undefined;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"flow-preview-threshold": FlowPreviewThreshold;
}
}

View File

@@ -126,6 +126,7 @@ class MoreInfoUpdate extends LitElement {
></ha-checkbox>
</ha-formfield> `
: ""}
<hr />
<div class="actions">
${this.stateObj.attributes.auto_update
? ""
@@ -239,20 +240,10 @@ class MoreInfoUpdate extends LitElement {
justify-content: space-between;
}
.actions {
border-top: 1px solid var(--divider-color);
background: var(
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
margin: 8px 0 0;
display: flex;
flex-wrap: wrap;
justify-content: center;
position: sticky;
bottom: 0;
padding: 12px 0;
margin-bottom: -24px;
z-index: 1;
}
.actions mwc-button {

View File

@@ -215,10 +215,10 @@ export class HaVoiceCommandDialog extends LitElement {
<div class="messages">
<div class="messages-container" id="scroll-container">
${this._conversation!.map(
// New lines matter for messages
// prettier-ignore
(message) => html`
<div class=${this._computeMessageClasses(message)}>${message.text}</div>
<div class=${this._computeMessageClasses(message)}>
${message.text}
</div>
`
)}
</div>
@@ -355,7 +355,7 @@ export class HaVoiceCommandDialog extends LitElement {
private _handleSendMessage() {
if (this._messageInput.value) {
this._processText(this._messageInput.value.trim());
this._processText(this._messageInput.value);
this._messageInput.value = "";
this._showSendButton = false;
}
@@ -427,28 +427,34 @@ export class HaVoiceCommandDialog extends LitElement {
private async _showNotSupportedMessage() {
this._addMessage({
who: "hass",
text:
// New lines matter for messages
// prettier-ignore
html`${this.hass.localize(
"ui.dialogs.voice_command.not_supported_microphone_browser"
)}
${this.hass.localize(
"ui.dialogs.voice_command.not_supported_microphone_documentation",
{
documentation_link: html`<a
target="_blank"
rel="noopener noreferrer"
href=${documentationUrl(
this.hass,
"/docs/configuration/securing/#remote-access"
)}
>${this.hass.localize(
"ui.dialogs.voice_command.not_supported_microphone_documentation_link"
)}</a>`,
}
)}`,
text: html`
<p>
${this.hass.localize(
"ui.dialogs.voice_command.not_supported_microphone_browser"
)}
</p>
<p>
${this.hass.localize(
"ui.dialogs.voice_command.not_supported_microphone_documentation",
{
documentation_link: html`
<a
target="_blank"
rel="noopener noreferrer"
href=${documentationUrl(
this.hass,
"/docs/configuration/securing/#remote-access"
)}
>
${this.hass.localize(
"ui.dialogs.voice_command.not_supported_microphone_documentation_link"
)}
</a>
`,
}
)}
</p>
`,
});
}
@@ -750,7 +756,6 @@ export class HaVoiceCommandDialog extends LitElement {
max-height: 100%;
}
.message {
white-space: pre-line;
font-size: 18px;
clear: both;
margin: 8px 0;
@@ -787,14 +792,10 @@ export class HaVoiceCommandDialog extends LitElement {
direction: var(--direction);
}
.message.user a {
.message a {
color: var(--text-primary-color);
}
.message.hass a {
color: var(--primary-text-color);
}
.message img {
width: 100%;
border-radius: 10px;

View File

@@ -2,6 +2,7 @@
import "../resources/compatibility";
import "../auth/ha-authorize";
import "../resources/safari-14-attachshadow-patch";
import "../resources/array.flat.polyfill";
import("../resources/ha-style");
import("@polymer/polymer/lib/utils/settings").then(

View File

@@ -25,6 +25,7 @@ import { subscribePanels } from "../data/ws-panels";
import { subscribeThemes } from "../data/ws-themes";
import { subscribeUser } from "../data/ws-user";
import type { ExternalAuth } from "../external_app/external_auth";
import "../resources/array.flat.polyfill";
import "../resources/safari-14-attachshadow-patch";
window.name = MAIN_WINDOW_NAME;

View File

@@ -2,6 +2,7 @@
import "../resources/compatibility";
import "../onboarding/ha-onboarding";
import "../resources/safari-14-attachshadow-patch";
import "../resources/array.flat.polyfill";
import("../resources/ha-style");
import("@polymer/polymer/lib/utils/settings").then(

View File

@@ -5,7 +5,6 @@ export const demoConfig: HassConfig = {
elevation: 300,
latitude: 52.3731339,
longitude: 4.8903147,
radius: 100,
unit_system: {
length: "km",
mass: "kg",

View File

@@ -96,7 +96,7 @@ export class HaConfigApplicationCredentials extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
back-path="/config"
backPath="/config"
.tabs=${configSections.devices}
.columns=${this._columns(this.narrow, this.hass.localize)}
.data=${this._getApplicationCredentials(

View File

@@ -12,8 +12,6 @@ import {
deviceAutomationsEqual,
DeviceCapabilities,
fetchDeviceActionCapabilities,
localizeExtraFieldsComputeLabelCallback,
localizeExtraFieldsComputeHelperCallback,
} from "../../../../../data/device_automation";
import { EntityRegistryEntry } from "../../../../../data/entity_registry";
import { HomeAssistant } from "../../../../../types";
@@ -86,13 +84,8 @@ export class HaDeviceAction extends LitElement {
.data=${this._extraFieldsData(this.action, this._capabilities)}
.schema=${this._capabilities.extra_fields}
.disabled=${this.disabled}
.computeLabel=${localizeExtraFieldsComputeLabelCallback(
this.hass,
this.action
)}
.computeHelper=${localizeExtraFieldsComputeHelperCallback(
this.hass,
this.action
.computeLabel=${this._extraFieldsComputeLabelCallback(
this.hass.localize
)}
@value-changed=${this._extraFieldsChanged}
></ha-form>
@@ -159,6 +152,14 @@ export class HaDeviceAction extends LitElement {
});
}
private _extraFieldsComputeLabelCallback(localize) {
// Returns a callback for ha-form to calculate labels per schema object
return (schema) =>
localize(
`ui.panel.config.automation.editor.actions.type.device_id.extra_fields.${schema.name}`
) || schema.name;
}
static styles = css`
ha-device-picker {
display: block;

View File

@@ -147,7 +147,7 @@ class DialogAutomationMode extends LitElement implements HassDialog {
type="number"
name="max"
.value=${this._newMax?.toString() ?? ""}
@input=${this._valueChanged}
@change=${this._valueChanged}
class="max"
>
</ha-textfield>

View File

@@ -12,8 +12,6 @@ import {
DeviceCapabilities,
DeviceCondition,
fetchDeviceConditionCapabilities,
localizeExtraFieldsComputeLabelCallback,
localizeExtraFieldsComputeHelperCallback,
} from "../../../../../data/device_automation";
import { EntityRegistryEntry } from "../../../../../data/entity_registry";
import type { HomeAssistant } from "../../../../../types";
@@ -86,13 +84,8 @@ export class HaDeviceCondition extends LitElement {
.data=${this._extraFieldsData(this.condition, this._capabilities)}
.schema=${this._capabilities.extra_fields}
.disabled=${this.disabled}
.computeLabel=${localizeExtraFieldsComputeLabelCallback(
this.hass,
this.condition
)}
.computeHelper=${localizeExtraFieldsComputeHelperCallback(
this.hass,
this.condition
.computeLabel=${this._extraFieldsComputeLabelCallback(
this.hass.localize
)}
@value-changed=${this._extraFieldsChanged}
></ha-form>
@@ -160,6 +153,14 @@ export class HaDeviceCondition extends LitElement {
});
}
private _extraFieldsComputeLabelCallback(localize) {
// Returns a callback for ha-form to calculate labels per schema object
return (schema) =>
localize(
`ui.panel.config.automation.editor.conditions.type.device.extra_fields.${schema.name}`
) || schema.name;
}
static styles = css`
ha-device-picker {
display: block;

View File

@@ -145,14 +145,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
@state() private _filteredAutomations?: string[] | null;
@storage({
storage: "sessionStorage",
key: "automation-table-search",
state: true,
subscribe: false,
})
private _filter = "";
@storage({
storage: "sessionStorage",
key: "automation-table-filters-full",
@@ -555,8 +547,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
"ui.panel.config.automation.picker.no_automations"
)}
@clear-filter=${this._clearFilter}
.filter=${this._filter}
@search-changed=${this._handleSearchChange}
hasFab
clickable
class=${this.narrow ? "narrow" : ""}
@@ -934,10 +924,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
this._applyFilters();
};
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
private _filterChanged(ev) {
const type = ev.target.localName;
this._filters = { ...this._filters, [type]: ev.detail };

View File

@@ -596,9 +596,6 @@ export default class HaAutomationTriggerRow extends LitElement {
private _showTriggeredInfo() {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.triggers.triggering_event_detail"
),
text: html`
<ha-yaml-editor
readOnly

View File

@@ -14,8 +14,6 @@ import {
DeviceCapabilities,
DeviceTrigger,
fetchDeviceTriggerCapabilities,
localizeExtraFieldsComputeLabelCallback,
localizeExtraFieldsComputeHelperCallback,
} from "../../../../../data/device_automation";
import { EntityRegistryEntry } from "../../../../../data/entity_registry";
import { HomeAssistant } from "../../../../../types";
@@ -90,13 +88,8 @@ export class HaDeviceTrigger extends LitElement {
.data=${this._extraFieldsData(this.trigger, this._capabilities)}
.schema=${this._capabilities.extra_fields}
.disabled=${this.disabled}
.computeLabel=${localizeExtraFieldsComputeLabelCallback(
this.hass,
this.trigger
)}
.computeHelper=${localizeExtraFieldsComputeHelperCallback(
this.hass,
this.trigger
.computeLabel=${this._extraFieldsComputeLabelCallback(
this.hass.localize
)}
@value-changed=${this._extraFieldsChanged}
></ha-form>
@@ -184,6 +177,14 @@ export class HaDeviceTrigger extends LitElement {
});
}
private _extraFieldsComputeLabelCallback(localize) {
// Returns a callback for ha-form to calculate labels per schema object
return (schema) =>
localize(
`ui.panel.config.automation.editor.triggers.type.device.extra_fields.${schema.name}`
) || schema.name;
}
static styles = css`
ha-device-picker {
display: block;

View File

@@ -107,14 +107,6 @@ class HaBlueprintOverview extends LitElement {
})
private _activeCollapsed?: string;
@storage({
storage: "sessionStorage",
key: "blueprint-table-search",
state: true,
subscribe: false,
})
private _filter: string = "";
private _processedBlueprints = memoizeOne(
(
blueprints: Record<string, Blueprints>,
@@ -316,8 +308,6 @@ class HaBlueprintOverview extends LitElement {
@sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
.filter=${this._filter}
@search-changed=${this._handleSearchChange}
>
<ha-icon-button
slot="toolbar-icon"
@@ -552,10 +542,6 @@ class HaBlueprintOverview extends LitElement {
this._activeCollapsed = ev.detail.value;
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
static get styles(): CSSResultGroup {
return haStyle;
}

View File

@@ -82,13 +82,10 @@ class HaConfigSectionUpdates extends LitElement {
>
${this.hass.localize("ui.panel.config.updates.show_skipped")}
</ha-check-list-item>
${this._supervisorInfo
${this._supervisorInfo && this._supervisorInfo.channel !== "dev"
? html`
<li divider role="separator"></li>
<mwc-list-item
@request-selected=${this._toggleBeta}
.disabled=${this._supervisorInfo.channel === "dev"}
>
<mwc-list-item @request-selected=${this._toggleBeta}>
${this._supervisorInfo.channel === "stable"
? this.hass.localize("ui.panel.config.updates.join_beta")
: this.hass.localize(

View File

@@ -6,7 +6,7 @@ import {
mdiPower,
mdiRefresh,
} from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { HassEntities, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
CSSResultGroup,
LitElement,
@@ -177,10 +177,7 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
protected render(): TemplateResult {
const { updates: canInstallUpdates, total: totalUpdates } =
this._filterUpdateEntitiesWithInstall(
this.hass.states,
this.hass.entities
);
this._filterUpdateEntitiesWithInstall(this.hass.states);
const { issues: repairsIssues, total: totalRepairIssues } =
this._repairsIssues;
@@ -309,13 +306,8 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
}
private _filterUpdateEntitiesWithInstall = memoizeOne(
(
entities: HomeAssistant["states"],
entityRegistry: HomeAssistant["entities"]
): { updates: UpdateEntity[]; total: number } => {
const updates = filterUpdateEntitiesWithInstall(entities).filter(
(entity) => !entityRegistry[entity.entity_id]?.hidden
);
(entities: HassEntities): { updates: UpdateEntity[]; total: number } => {
const updates = filterUpdateEntitiesWithInstall(entities);
return {
updates: updates.slice(0, updates.length === 3 ? updates.length : 2),

View File

@@ -24,7 +24,7 @@ import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { SENSOR_ENTITIES, ASSIST_ENTITIES } from "../../../common/const";
import { SENSOR_ENTITIES } from "../../../common/const";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
@@ -190,42 +190,26 @@ export class HaConfigDevicePage extends LitElement {
private _entitiesByCategory = memoizeOne(
(entities: EntityRegistryEntry[]) => {
const result = groupBy(entities, (entry) => {
const domain = computeDomain(entry.entity_id);
if (entry.entity_category) {
return entry.entity_category;
}
if (domain === "event" || domain === "notify") {
return domain;
}
if (SENSOR_ENTITIES.includes(domain)) {
return "sensor";
}
if (ASSIST_ENTITIES.includes(domain)) {
return "assist";
}
return "control";
}) as Record<
const result = groupBy(entities, (entry) =>
entry.entity_category
? entry.entity_category
: computeDomain(entry.entity_id) === "event"
? "event"
: SENSOR_ENTITIES.includes(computeDomain(entry.entity_id))
? "sensor"
: "control"
) as Record<
| "control"
| "event"
| "sensor"
| "assist"
| "notify"
| NonNullable<EntityRegistryEntry["entity_category"]>,
EntityRegistryStateEntry[]
>;
for (const key of [
"assist",
"config",
"control",
"diagnostic",
"event",
"notify",
"sensor",
]) {
if (!(key in result)) {
@@ -870,15 +854,7 @@ export class HaConfigDevicePage extends LitElement {
</div>
<div class="column">
${(
[
"control",
"sensor",
"notify",
"event",
"assist",
"config",
"diagnostic",
] as const
["control", "sensor", "event", "config", "diagnostic"] as const
).map((category) =>
// Make sure we render controls if no other cards will be rendered
entitiesByCategory[category].length > 0 ||
@@ -1028,9 +1004,6 @@ export class HaConfigDevicePage extends LitElement {
: this.hass.localize(
`ui.panel.config.devices.confirm_delete`
),
confirmText: this.hass.localize("ui.common.delete"),
dismissText: this.hass.localize("ui.common.cancel"),
destructive: true,
});
if (!confirmed) {

View File

@@ -122,13 +122,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
@state() private _selected: string[] = [];
@storage({
storage: "sessionStorage",
key: "devices-table-search",
state: true,
subscribe: false,
})
private _filter: string = history.state?.filter || "";
@state() private _filter: string = history.state?.filter || "";
@storage({
storage: "sessionStorage",
@@ -205,33 +199,38 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
}
private _setFiltersFromUrl() {
const domain = this._searchParms.get("domain");
const configEntry = this._searchParms.get("config_entry");
if (!domain && !configEntry) {
return;
if (this._searchParms.has("domain")) {
this._filters = {
...this._filters,
"ha-filter-states": {
value: [
...((this._filters["ha-filter-states"]?.value as string[]) || []),
"disabled",
],
items: undefined,
},
"ha-filter-integrations": {
value: [this._searchParms.get("domain")!],
items: undefined,
},
};
}
if (this._searchParms.has("config_entry")) {
this._filters = {
...this._filters,
"ha-filter-states": {
value: [
...((this._filters["ha-filter-states"]?.value as string[]) || []),
"disabled",
],
items: undefined,
},
config_entry: {
value: [this._searchParms.get("config_entry")!],
items: undefined,
},
};
}
this._filter = history.state?.filter || "";
this._filters = {
"ha-filter-states": {
value: [
...((this._filters["ha-filter-states"]?.value as string[]) || []),
"disabled",
],
items: undefined,
},
"ha-filter-integrations": {
value: domain ? [domain] : [],
items: undefined,
},
config_entry: {
value: configEntry ? [configEntry] : [],
items: undefined,
},
};
if (this._searchParms.has("label")) {
this._filterLabel();
}

View File

@@ -164,9 +164,6 @@ export class EntitySettingsHelperTab extends LitElement {
text: this.hass.localize(
"ui.dialogs.entity_registry.editor.confirm_delete"
),
confirmText: this.hass.localize("ui.common.delete"),
dismissText: this.hass.localize("ui.common.cancel"),
destructive: true,
}))
) {
return;

View File

@@ -215,9 +215,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
text: this.hass.localize(
"ui.dialogs.entity_registry.editor.confirm_delete"
),
confirmText: this.hass.localize("ui.common.delete"),
dismissText: this.hass.localize("ui.common.cancel"),
destructive: true,
}))
) {
return;

View File

@@ -144,13 +144,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
@consume({ context: fullEntitiesContext, subscribe: true })
_entities!: EntityRegistryEntry[];
@storage({
storage: "sessionStorage",
key: "entities-table-search",
state: true,
subscribe: false,
})
private _filter: string = history.state?.filter || "";
@state() private _filter: string = history.state?.filter || "";
@state() private _searchParms = new URLSearchParams(window.location.search);
@@ -709,6 +703,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
)
).length
}
.filter=${this._filter}
selectable
.selected=${this._selected.length}
.initialGroupColumn=${this._activeGrouping}
@@ -720,7 +715,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
@selection-changed=${this._handleSelectionChanged}
clickable
@clear-filter=${this._clearFilter}
.filter=${this._filter}
@search-changed=${this._handleSearchChange}
@row-click=${this._openEditEntry}
id="entity_id"
@@ -839,7 +833,7 @@ ${
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.delete_selected.button"
"ui.panel.config.entities.picker.remove_selected.button"
)}
</div>
</ha-menu-item>
@@ -949,7 +943,6 @@ ${
}
protected firstUpdated() {
this._setFiltersFromUrl();
if (Object.keys(this._filters).length) {
return;
}
@@ -959,36 +952,39 @@ ${
items: undefined,
},
};
this._setFiltersFromUrl();
fetchEntitySourcesWithCache(this.hass).then((sources) => {
this._entitySources = sources;
});
}
private _setFiltersFromUrl() {
const domain = this._searchParms.get("domain");
const configEntry = this._searchParms.get("config_entry");
if (!domain && !configEntry) {
return;
if (this._searchParms.has("domain")) {
this._filters = {
...this._filters,
"ha-filter-states": {
value: [],
items: undefined,
},
"ha-filter-integrations": {
value: [this._searchParms.get("domain")!],
items: undefined,
},
};
}
if (this._searchParms.has("config_entry")) {
this._filters = {
...this._filters,
"ha-filter-states": {
value: [],
items: undefined,
},
config_entry: {
value: [this._searchParms.get("config_entry")!],
items: undefined,
},
};
}
this._filter = history.state?.filter || "";
this._filters = {
"ha-filter-states": {
value: [],
items: undefined,
},
"ha-filter-integrations": {
value: domain ? [domain] : [],
items: undefined,
},
config_entry: {
value: configEntry ? [configEntry] : [],
items: undefined,
},
};
if (this._searchParms.has("label")) {
this._filterLabel();
}
@@ -1256,23 +1252,25 @@ ${rejected
});
showConfirmationDialog(this, {
title: this.hass.localize(
`ui.panel.config.entities.picker.delete_selected.confirm_title`
`ui.panel.config.entities.picker.remove_selected.confirm_${
removeableEntities.length !== this._selected.length ? "partly_" : ""
}title`,
{ number: removeableEntities.length }
),
text:
removeableEntities.length === this._selected.length
? this.hass.localize(
"ui.panel.config.entities.picker.delete_selected.confirm_text"
"ui.panel.config.entities.picker.remove_selected.confirm_text"
)
: this.hass.localize(
"ui.panel.config.entities.picker.delete_selected.confirm_partly_text",
"ui.panel.config.entities.picker.remove_selected.confirm_partly_text",
{
deletable: removeableEntities.length,
removable: removeableEntities.length,
selected: this._selected.length,
}
),
confirmText: this.hass.localize("ui.common.delete"),
confirmText: this.hass.localize("ui.common.remove"),
dismissText: this.hass.localize("ui.common.cancel"),
destructive: true,
confirm: () => {
removeableEntities.forEach((entity) =>
removeEntityRegistryEntry(this.hass, entity)

View File

@@ -159,14 +159,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
})
private _activeCollapsed?: string;
@storage({
storage: "sessionStorage",
key: "helpers-table-search",
state: true,
subscribe: false,
})
private _filter = "";
@state() private _stateItems: HassEntity[] = [];
@state() private _entityEntries?: Record<string, EntityRegistryEntry>;
@@ -567,8 +559,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
.activeFilters=${this._activeFilters}
@clear-filter=${this._clearFilter}
@row-click=${this._openEditDialog}
.filter=${this._filter}
@search-changed=${this._handleSearchChange}
hasFab
clickable
.noDataText=${this.hass.localize(
@@ -1080,10 +1070,6 @@ ${rejected
this._activeCollapsed = ev.detail.value;
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@@ -1,6 +1,6 @@
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import "@material/mwc-list/mwc-list";
import "@material/web/divider/divider";
import { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item-base";
import {
mdiAlertCircle,
mdiBookshelf,
@@ -27,13 +27,13 @@ import {
} from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
TemplateResult,
css,
html,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -41,15 +41,15 @@ import { until } from "lit/directives/until";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { isDevVersion } from "../../../common/config/version";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import { nextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-button-menu-new";
import "../../../components/ha-card";
import "../../../components/ha-list-item";
import "../../../components/ha-list-item-new";
import "../../../components/ha-list-new";
import "../../../components/ha-menu-item";
import "../../../components/ha-list-item-new";
import "../../../components/ha-button-menu-new";
import {
deleteApplicationCredential,
fetchApplicationCredentialsConfigEntry,
@@ -57,13 +57,13 @@ import {
import { getSignedPath } from "../../../data/auth";
import {
ConfigEntry,
DisableConfigEntryResult,
ERROR_STATES,
RECOVERABLE_STATES,
deleteConfigEntry,
disableConfigEntry,
DisableConfigEntryResult,
enableConfigEntry,
ERROR_STATES,
getConfigEntries,
RECOVERABLE_STATES,
reloadConfigEntry,
updateConfigEntry,
} from "../../../data/config_entries";
@@ -80,13 +80,13 @@ import {
} from "../../../data/entity_registry";
import { getErrorLogDownloadUrl } from "../../../data/error_log";
import {
IntegrationLogInfo,
IntegrationManifest,
LogSeverity,
domainToName,
fetchIntegrationManifest,
integrationIssuesUrl,
IntegrationLogInfo,
IntegrationManifest,
integrationsWithPanel,
LogSeverity,
setIntegrationLogLevel,
subscribeLogInfo,
} from "../../../data/integration";
@@ -453,7 +453,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
const attention = ATTENTION_SOURCES.includes(
flow.context.source
);
return html` <ha-list-item-new
return html`<ha-list-item-new
class="config_entry ${attention ? "attention" : ""}"
>
${flow.localized_title}
@@ -477,15 +477,8 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
></ha-button>
</ha-list-item-new>`;
})}
${attentionEntries.map(
(item, index) =>
html`${this._renderConfigEntry(item)}
${index < attentionEntries.length - 1
? html` <md-divider
role="separator"
tabindex="-1"
></md-divider>`
: ""} `
${attentionEntries.map((item) =>
this._renderConfigEntry(item)
)}
</ha-list-new>
</ha-card>`
@@ -509,16 +502,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
</div>`
: nothing}
<ha-list-new>
${normalEntries.map(
(item, index) =>
html`${this._renderConfigEntry(item)}
${index < normalEntries.length - 1
? html` <md-divider
role="separator"
tabindex="-1"
></md-divider>`
: ""} `
)}
${normalEntries.map((item) => this._renderConfigEntry(item))}
</ha-list-new>
<div class="card-actions">
<ha-button @click=${this._addIntegration}>
@@ -683,21 +667,19 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
const configPanel = this._configPanel(item.domain, this.hass.panels);
return html` <ha-list-item-new
return html`<ha-list-item-new
class=${classMap({
config_entry: true,
"state-not-loaded": item!.state === "not_loaded",
"state-failed-unload": item!.state === "failed_unload",
"state-setup": item!.state === "setup_in_progress",
"state-error": ERROR_STATES.includes(item!.state),
"state-disabled": item.disabled_by !== null,
})}
data-entry-id=${item.entry_id}
.disabled=${item.disabled_by}
.configEntry=${item}
>
<div slot="headline">
${item.title || domainToName(this.hass.localize, item.domain)}
</div>
${item.title || domainToName(this.hass.localize, item.domain)}
<div slot="supporting-text">
<div>${devicesLine}</div>
${stateText
@@ -744,159 +726,170 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
.path=${mdiDotsVertical}
></ha-icon-button>
${item.supports_options && stateText
? html`<ha-menu-item @click=${this._showOptions}>
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
? html`<ha-list-item
@request-selected=${this._showOptions}
graphic="icon"
>
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.configure"
)}
</ha-menu-item>`
</ha-list-item>`
: ""}
${item.disabled_by && devices.length
? html`
<ha-menu-item
href=${devices.length === 1
? `/config/devices/device/${devices[0].id}`
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
>
<ha-svg-icon .path=${mdiDevices} slot="start"></ha-svg-icon>
? html`<a
href=${devices.length === 1
? `/config/devices/device/${devices[0].id}`
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
>
<ha-list-item hasMeta graphic="icon">
<ha-svg-icon .path=${mdiDevices} slot="graphic"></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.devices`,
{ count: devices.length }
)}
<ha-icon-next slot="end"></ha-icon-next>
</ha-menu-item>
`
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>`
: ""}
${item.disabled_by && services.length
? html`<ha-menu-item
? html`<a
href=${services.length === 1
? `/config/devices/device/${services[0].id}`
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
>
<ha-svg-icon
.path=${mdiHandExtendedOutline}
slot="start"
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.services`,
{ count: services.length }
)}
<ha-icon-next slot="end"></ha-icon-next>
</ha-menu-item> `
<ha-list-item hasMeta graphic="icon">
<ha-svg-icon
.path=${mdiHandExtendedOutline}
slot="graphic"
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.services`,
{ count: services.length }
)}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>`
: ""}
${item.disabled_by && entities.length
? html`
<ha-menu-item
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
>
? html`<a
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
>
<ha-list-item hasMeta graphic="icon">
<ha-svg-icon
.path=${mdiShapeOutline}
slot="start"
slot="graphic"
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.entities`,
{ count: entities.length }
)}
<ha-icon-next slot="end"></ha-icon-next>
</ha-menu-item>
`
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>`
: ""}
${!item.disabled_by &&
RECOVERABLE_STATES.includes(item.state) &&
item.supports_unload &&
item.source !== "system"
? html`
<ha-menu-item @click=${this._handleReload}>
<ha-svg-icon slot="start" .path=${mdiReload}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.reload"
)}
</ha-menu-item>
`
: nothing}
? html`<ha-list-item
@request-selected=${this._handleReload}
graphic="icon"
>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.reload"
)}
<ha-svg-icon slot="graphic" .path=${mdiReload}></ha-svg-icon>
</ha-list-item>`
: ""}
<ha-menu-item @click=${this._handleRename} graphic="icon">
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
<ha-list-item @request-selected=${this._handleRename} graphic="icon">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.rename"
)}
</ha-menu-item>
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
</ha-list-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<li divider role="separator"></li>
${this._diagnosticHandler && item.state === "loaded"
? html`
<ha-menu-item
href=${getConfigEntryDiagnosticsDownloadUrl(item.entry_id)}
target="_blank"
@click=${this._signUrl}
>
<ha-svg-icon slot="start" .path=${mdiDownload}></ha-svg-icon>
? html`<a
href=${getConfigEntryDiagnosticsDownloadUrl(item.entry_id)}
target="_blank"
@click=${this._signUrl}
>
<ha-list-item graphic="icon">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.download_diagnostics"
)}
</ha-menu-item>
`
<ha-svg-icon slot="graphic" .path=${mdiDownload}></ha-svg-icon>
</ha-list-item>
</a>`
: ""}
${!item.disabled_by &&
item.supports_reconfigure &&
item.source !== "system"
? html`
<ha-menu-item @click=${this._handleReconfigure}>
<ha-svg-icon slot="start" .path=${mdiWrench}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.reconfigure"
)}
</ha-menu-item>
`
: nothing}
? html`<ha-list-item
@request-selected=${this._handleReconfigure}
graphic="icon"
>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.reconfigure"
)}
<ha-svg-icon slot="graphic" .path=${mdiWrench}></ha-svg-icon>
</ha-list-item>`
: ""}
<ha-menu-item @click=${this._handleSystemOptions} graphic="icon">
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
<ha-list-item
@request-selected=${this._handleSystemOptions}
graphic="icon"
>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.system_options"
)}
</ha-menu-item>
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
</ha-list-item>
${item.disabled_by === "user"
? html`
<ha-menu-item @click=${this._handleEnable}>
<ha-svg-icon
slot="start"
.path=${mdiPlayCircleOutline}
></ha-svg-icon>
${this.hass.localize("ui.common.enable")}
</ha-menu-item>
`
? html`<ha-list-item
@request-selected=${this._handleEnable}
graphic="icon"
>
${this.hass.localize("ui.common.enable")}
<ha-svg-icon
slot="graphic"
.path=${mdiPlayCircleOutline}
></ha-svg-icon>
</ha-list-item>`
: item.source !== "system"
? html`
<ha-menu-item
class="warning"
@click=${this._handleDisable}
graphic="icon"
>
<ha-svg-icon
slot="start"
class="warning"
.path=${mdiStopCircleOutline}
></ha-svg-icon>
${this.hass.localize("ui.common.disable")}
</ha-menu-item>
`
: nothing}
${item.source !== "system"
? html`
<ha-menu-item class="warning" @click=${this._handleDelete}>
? html`<ha-list-item
class="warning"
@request-selected=${this._handleDisable}
graphic="icon"
>
${this.hass.localize("ui.common.disable")}
<ha-svg-icon
slot="start"
slot="graphic"
class="warning"
.path=${mdiDelete}
.path=${mdiStopCircleOutline}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.delete"
)}
</ha-menu-item>
`
: nothing}
</ha-list-item>`
: ""}
${item.source !== "system"
? html`<ha-list-item
class="warning"
@request-selected=${this._handleDelete}
graphic="icon"
>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.delete"
)}
<ha-svg-icon
slot="graphic"
class="warning"
.path=${mdiDelete}
></ha-svg-icon>
</ha-list-item>`
: ""}
</ha-button-menu-new>
</ha-list-item-new>`;
}
@@ -1062,43 +1055,64 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
);
}
private _handleRename(ev: Event): void {
private _handleRename(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._editEntryName(
((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
);
}
private _handleReload(ev: Event): void {
private _handleReload(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._reloadIntegration(
((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
);
}
private _handleReconfigure(ev: Event): void {
private _handleReconfigure(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._reconfigureIntegration(
((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
);
}
private _handleDelete(ev: Event): void {
private _handleDelete(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._removeIntegration(
((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
);
}
private _handleDisable(ev: Event): void {
private _handleDisable(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._disableIntegration(
((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
);
}
private _handleEnable(ev: Event): void {
private _handleEnable(ev: CustomEvent<RequestSelectedDetail>): void {
if (ev.detail.source && !shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._enableIntegration(
((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
);
}
private _handleSystemOptions(ev: Event): void {
private _handleSystemOptions(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._showSystemOptions(
((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
);
@@ -1313,7 +1327,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
}
private async _signUrl(ev) {
const anchor = ev.currentTarget;
const anchor = ev.target.closest("a");
ev.preventDefault();
const signedUrl = await getSignedPath(
this.hass,
@@ -1414,22 +1428,32 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
ha-alert:first-of-type {
margin-top: 16px;
}
ha-list-item-new {
position: relative;
}
ha-list-item-new.discovered {
--mdc-list-item-meta-size: auto;
--mdc-list-item-meta-display: flex;
height: 72px;
}
ha-list-item-new.config_entry {
overflow: visible;
--mdc-list-item-meta-size: auto;
--mdc-list-item-meta-display: flex;
}
ha-button-menu-new ha-list-item {
--mdc-list-item-meta-size: 24px;
}
ha-list-item-new.config_entry::after {
position: absolute;
top: 0;
top: 8px;
right: 0;
bottom: 0;
bottom: 8px;
left: 0;
opacity: 0.12;
pointer-events: none;
content: "";
}
ha-button-menu-new {
flex: 0;
}
a {
text-decoration: none;
}
@@ -1486,10 +1510,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
overflow: hidden;
text-overflow: ellipsis;
}
.state-disabled [slot="headline"],
.state-disabled [slot="supporting-text"] {
opacity: var(--md-list-item-disabled-opacity, 0.3);
}
ha-list-new {
margin-top: 8px;
margin-bottom: 8px;

View File

@@ -36,8 +36,14 @@ class DialogThreadDataset extends LitElement implements HassDialog {
dataset.extended_pan_id &&
otbrInfo.active_dataset_tlvs?.includes(dataset.extended_pan_id);
const canImportKeychain =
hasOTBR &&
!this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain &&
network.routers?.length;
return html`<ha-dialog
open
.hideActions=${!canImportKeychain}
@closed=${this.closeDialog}
.heading=${createCloseHeading(this.hass, network.name)}
>
@@ -53,8 +59,28 @@ class DialogThreadDataset extends LitElement implements HassDialog {
Active dataset TLVs: ${otbrInfo.active_dataset_tlvs}`
: nothing}
</div>
${canImportKeychain
? html`<ha-button slot="primary-action" @click=${this._sendCredentials}
>Send credentials to phone</ha-button
>`
: nothing}
</ha-dialog>`;
}
private _sendCredentials() {
this.hass.auth.external!.fireMessage({
type: "thread/store_in_platform_keychain",
payload: {
mac_extended_address:
this._params?.network.dataset?.preferred_extended_address ||
this._params!.network.routers![0]!.extended_address,
border_agent_id:
this._params?.network.dataset?.preferred_border_agent_id ||
this._params!.network.routers![0]!.border_agent_id,
active_operational_dataset: this._params!.otbrInfo!.active_dataset_tlvs,
},
});
}
}
declare global {

View File

@@ -151,7 +151,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
slot="fab"
@click=${this._importExternalThreadCredentials}
extended
label="Send credentials to Home Assistant"
label="Import credentials"
><ha-svg-icon slot="icon" .path=${mdiCellphoneKey}></ha-svg-icon
></ha-fab>`
: nothing}
@@ -160,14 +160,6 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
}
private _renderNetwork(network: ThreadNetwork) {
const canImportKeychain =
this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain &&
network.dataset?.extended_pan_id &&
this._otbrInfo &&
this._otbrInfo?.active_dataset_tlvs?.includes(
network.dataset.extended_pan_id
);
return html`<ha-card>
<div class="card-header">
${network.name}${network.dataset
@@ -311,30 +303,9 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
>
</div>`
: ""}
${canImportKeychain
? html`<div class="card-actions">
<mwc-button @click=${this._sendCredentials}
>Send credentials to phone</mwc-button
>
</div>`
: ""}
</ha-card>`;
}
private _sendCredentials() {
if (!this._otbrInfo) {
return;
}
this.hass.auth.external!.fireMessage({
type: "thread/store_in_platform_keychain",
payload: {
mac_extended_address: this._otbrInfo.extended_address,
border_agent_id: this._otbrInfo.border_agent_id ?? "",
active_operational_dataset: this._otbrInfo.active_dataset_tlvs ?? "",
},
});
}
private async _showDatasetInfo(ev: Event) {
const network = (ev.currentTarget as any).network as ThreadNetwork;
showThreadDatasetDialog(this, { network, otbrInfo: this._otbrInfo });

View File

@@ -3,7 +3,6 @@ import { mdiAlertCircle, mdiCheckCircle, mdiQrcodeScan } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-alert";
import type { HaCheckbox } from "../../../../../components/ha-checkbox";
@@ -61,8 +60,7 @@ class DialogZWaveJSAddNode extends LitElement {
| "finished"
| "provisioned"
| "validate_dsk_enter_pin"
| "grant_security_classes"
| "waiting_for_device";
| "grant_security_classes";
@state() private _device?: ZWaveJSAddNodeDevice;
@@ -88,11 +86,6 @@ class DialogZWaveJSAddNode extends LitElement {
private _qrProcessing = false;
public connectedCallback(): void {
super.connectedCallback();
window.addEventListener("beforeunload", this._onBeforeUnload);
}
public disconnectedCallback(): void {
super.disconnectedCallback();
this._unsubscribe();
@@ -113,22 +106,14 @@ class DialogZWaveJSAddNode extends LitElement {
return nothing;
}
// Prevent accidentally closing the dialog in certain stages
const preventClose = this._shouldPreventClose();
const heading = this.hass.localize(
"ui.panel.config.zwave_js.add_node.title"
);
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${preventClose
? heading
: createCloseHeading(this.hass, heading)}
scrimClickAction=${ifDefined(preventClose ? "" : undefined)}
escapeKeyAction=${ifDefined(preventClose ? "" : undefined)}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.zwave_js.add_node.title")
)}
>
${this._status === "loading"
? html`<div style="display: flex; justify-content: center;">
@@ -137,93 +122,81 @@ class DialogZWaveJSAddNode extends LitElement {
indeterminate
></ha-circular-progress>
</div>`
: this._status === "waiting_for_device"
? html`<div class="flex-container">
<ha-circular-progress indeterminate></ha-circular-progress>
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.waiting_for_device"
)}
</p>
</div>`
: this._status === "choose_strategy"
? html`<h3>Choose strategy</h3>
<div class="flex-column">
<ha-formfield
.label=${html`<b>Secure if possible</b>
<div class="secondary">
Requires user interaction during inclusion. Fast and
secure with S2 when supported. Fallback to legacy S0
or no encryption when necessary.
</div>`}
>
<ha-radio
name="strategy"
@change=${this._handleStrategyChange}
.value=${InclusionStrategy.Default}
.checked=${this._inclusionStrategy ===
InclusionStrategy.Default ||
this._inclusionStrategy === undefined}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${html`<b>Legacy Secure</b>
<div class="secondary">
Uses the older S0 security that is secure, but slow
due to a lot of overhead. Allows securely including S2
capable devices which fail to be included with S2.
</div>`}
>
<ha-radio
name="strategy"
@change=${this._handleStrategyChange}
.value=${InclusionStrategy.Security_S0}
.checked=${this._inclusionStrategy ===
InclusionStrategy.Security_S0}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${html`<b>Insecure</b>
<div class="secondary">Do not use encryption.</div>`}
>
<ha-radio
name="strategy"
@change=${this._handleStrategyChange}
.value=${InclusionStrategy.Insecure}
.checked=${this._inclusionStrategy ===
InclusionStrategy.Insecure}
>
</ha-radio>
</ha-formfield>
</div>
<mwc-button
slot="primaryAction"
@click=${this._startManualInclusion}
: this._status === "choose_strategy"
? html`<h3>Choose strategy</h3>
<div class="flex-column">
<ha-formfield
.label=${html`<b>Secure if possible</b>
<div class="secondary">
Requires user interaction during inclusion. Fast and
secure with S2 when supported. Fallback to legacy S0 or
no encryption when necessary.
</div>`}
>
Search device
</mwc-button>`
: this._status === "qr_scan"
? html`${this._error
? html`<ha-alert alert-type="error"
>${this._error}</ha-alert
>`
: ""}
<ha-qr-scanner
.localize=${this.hass.localize}
@qr-code-scanned=${this._qrCodeScanned}
></ha-qr-scanner>
<mwc-button
slot="secondaryAction"
@click=${this._startOver}
<ha-radio
name="strategy"
@change=${this._handleStrategyChange}
.value=${InclusionStrategy.Default}
.checked=${this._inclusionStrategy ===
InclusionStrategy.Default ||
this._inclusionStrategy === undefined}
>
${this.hass.localize(
"ui.panel.config.zwave_js.common.back"
)}
</mwc-button>`
: this._status === "validate_dsk_enter_pin"
? html`
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${html`<b>Legacy Secure</b>
<div class="secondary">
Uses the older S0 security that is secure, but slow due
to a lot of overhead. Allows securely including S2
capable devices which fail to be included with S2.
</div>`}
>
<ha-radio
name="strategy"
@change=${this._handleStrategyChange}
.value=${InclusionStrategy.Security_S0}
.checked=${this._inclusionStrategy ===
InclusionStrategy.Security_S0}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${html`<b>Insecure</b>
<div class="secondary">Do not use encryption.</div>`}
>
<ha-radio
name="strategy"
@change=${this._handleStrategyChange}
.value=${InclusionStrategy.Insecure}
.checked=${this._inclusionStrategy ===
InclusionStrategy.Insecure}
>
</ha-radio>
</ha-formfield>
</div>
<mwc-button
slot="primaryAction"
@click=${this._startManualInclusion}
>
Search device
</mwc-button>`
: this._status === "qr_scan"
? html`${this._error
? html`<ha-alert alert-type="error"
>${this._error}</ha-alert
>`
: ""}
<ha-qr-scanner
.localize=${this.hass.localize}
@qr-code-scanned=${this._qrCodeScanned}
></ha-qr-scanner>
<mwc-button slot="secondaryAction" @click=${this._startOver}>
${this.hass.localize(
"ui.panel.config.zwave_js.common.back"
)}
</mwc-button>`
: this._status === "validate_dsk_enter_pin"
? html`
<p>
Please enter the 5-digit PIN for your device and verify that
the rest of the device-specific key matches the one that can
@@ -252,160 +225,198 @@ class DialogZWaveJSAddNode extends LitElement {
</mwc-button>
</div>
`
: this._status === "grant_security_classes"
? html`
<h3>
The device has requested the following security
classes:
</h3>
${this._error
? html`<ha-alert alert-type="error"
>${this._error}</ha-alert
>`
: ""}
<div class="flex-column">
${this._requestedGrant?.securityClasses
.sort((a, b) => {
// Put highest security classes at the top, S0 at the bottom
if (a === SecurityClass.S0_Legacy) return 1;
if (b === SecurityClass.S0_Legacy) return -1;
return b - a;
})
.map(
(securityClass) =>
html`<ha-formfield
.label=${html`<b
>${this.hass.localize(
`ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.title`
)}</b
>
<div class="secondary">
${this.hass.localize(
`ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.description`
)}
</div>`}
>
<ha-checkbox
@change=${this._handleSecurityClassChange}
.value=${securityClass}
.checked=${this._securityClasses.includes(
securityClass
)}
: this._status === "grant_security_classes"
? html`
<h3>
The device has requested the following security classes:
</h3>
${this._error
? html`<ha-alert alert-type="error"
>${this._error}</ha-alert
>`
: ""}
<div class="flex-column">
${this._requestedGrant?.securityClasses
.sort((a, b) => {
// Put highest security classes at the top, S0 at the bottom
if (a === SecurityClass.S0_Legacy) return 1;
if (b === SecurityClass.S0_Legacy) return -1;
return b - a;
})
.map(
(securityClass) =>
html`<ha-formfield
.label=${html`<b
>${this.hass.localize(
`ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.title`
)}</b
>
</ha-checkbox>
</ha-formfield>`
)}
</div>
<div class="secondary">
${this.hass.localize(
`ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.description`
)}
</div>`}
>
<ha-checkbox
@change=${this._handleSecurityClassChange}
.value=${securityClass}
.checked=${this._securityClasses.includes(
securityClass
)}
>
</ha-checkbox>
</ha-formfield>`
)}
</div>
<mwc-button
slot="primaryAction"
.disabled=${!this._securityClasses.length}
@click=${this._grantSecurityClasses}
>
Submit
</mwc-button>
`
: this._status === "timed_out"
? html`
<h3>Timed out!</h3>
<p>
We have not found any device in inclusion mode. Make
sure the device is active and in inclusion mode.
</p>
<mwc-button
slot="primaryAction"
.disabled=${!this._securityClasses.length}
@click=${this._grantSecurityClasses}
@click=${this._startOver}
>
Submit
Retry
</mwc-button>
`
: this._status === "timed_out"
? html`
<h3>Timed out!</h3>
: this._status === "started_specific"
? html`<h3>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.searching_device"
)}
</h3>
<ha-circular-progress
indeterminate
></ha-circular-progress>
<p>
We have not found any device in inclusion mode. Make
sure the device is active and in inclusion mode.
</p>
<mwc-button
slot="primaryAction"
@click=${this._startOver}
>
Retry
</mwc-button>
`
: this._status === "started_specific"
? html`<h3>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.searching_device"
)}
</h3>
<ha-circular-progress
indeterminate
></ha-circular-progress>
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
)}
</p>`
: this._status === "started"
? html`
<div class="select-inclusion">
<div class="outline">
<h2>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
)}
</p>`
: this._status === "started"
? html`
<div class="select-inclusion">
<div class="outline">
<h2>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.searching_device"
)}
</h2>
<ha-circular-progress
indeterminate
></ha-circular-progress>
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
)}
</p>
<p>
<button
class="link"
@click=${this._chooseInclusionStrategy}
>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.searching_device"
"ui.panel.config.zwave_js.add_node.choose_inclusion_strategy"
)}
</h2>
<ha-circular-progress
indeterminate
></ha-circular-progress>
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
)}
</p>
<p>
<button
class="link"
@click=${this._chooseInclusionStrategy}
>
</button>
</p>
</div>
${this._supportsSmartStart
? html` <div class="outline">
<h2>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.choose_inclusion_strategy"
"ui.panel.config.zwave_js.add_node.qr_code"
)}
</button>
</h2>
<ha-svg-icon
.path=${mdiQrcodeScan}
></ha-svg-icon>
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.qr_code_paragraph"
)}
</p>
<p>
<mwc-button @click=${this._scanQRCode}>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.scan_qr_code"
)}
</mwc-button>
</p>
</div>`
: ""}
</div>
<mwc-button
slot="primaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</mwc-button>
`
: this._status === "interviewing"
? html`
<div class="flex-container">
<ha-circular-progress
indeterminate
></ha-circular-progress>
<div class="status">
<p>
<b
>${this.hass.localize(
"ui.panel.config.zwave_js.add_node.interview_started"
)}</b
>
</p>
${this._stages
? html` <div class="stages">
${this._stages.map(
(stage) => html`
<span class="stage">
<ha-svg-icon
.path=${mdiCheckCircle}
class="success"
></ha-svg-icon>
${stage}
</span>
`
)}
</div>`
: ""}
</div>
${this._supportsSmartStart
? html` <div class="outline">
<h2>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.qr_code"
)}
</h2>
<ha-svg-icon
.path=${mdiQrcodeScan}
></ha-svg-icon>
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.qr_code_paragraph"
)}
</p>
<p>
<mwc-button @click=${this._scanQRCode}>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.scan_qr_code"
)}
</mwc-button>
</p>
</div>`
: ""}
</div>
<mwc-button
slot="primaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "interviewing"
: this._status === "failed"
? html`
<div class="flex-container">
<ha-circular-progress
indeterminate
></ha-circular-progress>
<div class="status">
<p>
<b
>${this.hass.localize(
"ui.panel.config.zwave_js.add_node.interview_started"
)}</b
>
</p>
<ha-alert
alert-type="error"
.title=${this.hass.localize(
"ui.panel.config.zwave_js.add_node.inclusion_failed"
)}
>
${this._error ||
this.hass.localize(
"ui.panel.config.zwave_js.add_node.check_logs"
)}
</ha-alert>
${this._stages
? html` <div class="stages">
${this._stages.map(
@@ -430,21 +441,45 @@ class DialogZWaveJSAddNode extends LitElement {
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "failed"
: this._status === "finished"
? html`
<div class="flex-container">
<ha-svg-icon
.path=${this._lowSecurity
? mdiAlertCircle
: mdiCheckCircle}
class=${this._lowSecurity
? "warning"
: "success"}
></ha-svg-icon>
<div class="status">
<ha-alert
alert-type="error"
.title=${this.hass.localize(
"ui.panel.config.zwave_js.add_node.inclusion_failed"
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.inclusion_finished"
)}
</p>
${this._lowSecurity
? html`<ha-alert
alert-type="warning"
title="The device was added insecurely"
>
There was an error during secure
inclusion. You can try again by
excluding the device and adding it
again.
</ha-alert>`
: ""}
<a
href=${`/config/devices/device/${
this._device!.id
}`}
>
${this._error ||
this.hass.localize(
"ui.panel.config.zwave_js.add_node.check_logs"
)}
</ha-alert>
<mwc-button>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.view_device"
)}
</mwc-button>
</a>
${this._stages
? html` <div class="stages">
${this._stages.map(
@@ -469,60 +504,18 @@ class DialogZWaveJSAddNode extends LitElement {
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "finished"
? html`
<div class="flex-container">
: this._status === "provisioned"
? html` <div class="flex-container">
<ha-svg-icon
.path=${this._lowSecurity
? mdiAlertCircle
: mdiCheckCircle}
class=${this._lowSecurity
? "warning"
: "success"}
.path=${mdiCheckCircle}
class="success"
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.inclusion_finished"
"ui.panel.config.zwave_js.add_node.provisioning_finished"
)}
</p>
${this._lowSecurity
? html`<ha-alert
alert-type="warning"
title="The device was added insecurely"
>
There was an error during secure
inclusion. You can try again by
excluding the device and adding it
again.
</ha-alert>`
: ""}
<a
href=${`/config/devices/device/${
this._device?.id
}`}
>
<mwc-button>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.view_device"
)}
</mwc-button>
</a>
${this._stages
? html` <div class="stages">
${this._stages.map(
(stage) => html`
<span class="stage">
<ha-svg-icon
.path=${mdiCheckCircle}
class="success"
></ha-svg-icon>
${stage}
</span>
`
)}
</div>`
: ""}
</div>
</div>
<mwc-button
@@ -530,42 +523,12 @@ class DialogZWaveJSAddNode extends LitElement {
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "provisioned"
? html` <div class="flex-container">
<ha-svg-icon
.path=${mdiCheckCircle}
class="success"
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.provisioning_finished"
)}
</p>
</div>
</div>
<mwc-button
slot="primaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.close")}
</mwc-button>`
: ""}
</mwc-button>`
: ""}
</ha-dialog>
`;
}
private _shouldPreventClose(): boolean {
return (
this._status === "started_specific" ||
this._status === "validate_dsk_enter_pin" ||
this._status === "grant_security_classes" ||
this._status === "waiting_for_device"
);
}
private _chooseInclusionStrategy(): void {
this._unsubscribe();
this._status = "choose_strategy";
@@ -676,7 +639,7 @@ class DialogZWaveJSAddNode extends LitElement {
}
private async _validateDskAndEnterPin(): Promise<void> {
this._status = "waiting_for_device";
this._status = "loading";
this._error = undefined;
try {
await zwaveValidateDskAndEnterPin(
@@ -693,7 +656,7 @@ class DialogZWaveJSAddNode extends LitElement {
}
private async _grantSecurityClasses(): Promise<void> {
this._status = "waiting_for_device";
this._status = "loading";
this._error = undefined;
try {
await zwaveGrantSecurityClasses(
@@ -756,12 +719,6 @@ class DialogZWaveJSAddNode extends LitElement {
this._addNodeTimeoutHandle = undefined;
}
if (message.event === "node found") {
// The user may have to enter a PIN. Until then prevent accidentally
// closing the dialog
this._status = "waiting_for_device";
}
if (message.event === "validate dsk and enter pin") {
this._status = "validate_dsk_enter_pin";
this._dsk = message.dsk;
@@ -818,13 +775,6 @@ class DialogZWaveJSAddNode extends LitElement {
}, 90000);
}
private _onBeforeUnload = (event: BeforeUnloadEvent) => {
if (this._shouldPreventClose()) {
event.preventDefault();
}
event.returnValue = true;
};
private _unsubscribe(): void {
if (this._subscribed) {
this._subscribed.then((unsub) => unsub());
@@ -841,7 +791,6 @@ class DialogZWaveJSAddNode extends LitElement {
clearTimeout(this._addNodeTimeoutHandle);
}
this._addNodeTimeoutHandle = undefined;
window.removeEventListener("beforeunload", this._onBeforeUnload);
}
public closeDialog(): void {

View File

@@ -51,14 +51,6 @@ export class HaConfigLabels extends LitElement {
@state() private _labels: LabelRegistryEntry[] = [];
@storage({
storage: "sessionStorage",
key: "labels-table-search",
state: true,
subscribe: false,
})
private _filter = "";
@storage({
key: "labels-table-sort",
state: false,
@@ -168,8 +160,6 @@ export class HaConfigLabels extends LitElement {
hasFab
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
.filter=${this._filter}
@search-changed=${this._handleSearchChange}
@row-click=${this._editLabel}
clickable
id="label_id"
@@ -293,10 +283,6 @@ export class HaConfigLabels extends LitElement {
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
}
declare global {

View File

@@ -2,21 +2,17 @@ import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import {
mdiCheck,
mdiCheckCircleOutline,
mdiDelete,
mdiDotsVertical,
mdiPencil,
mdiOpenInNew,
mdiPlus,
mdiStar,
} from "@mdi/js";
import { LitElement, PropertyValues, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoize from "memoize-one";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import { storage } from "../../../../common/decorators/storage";
import { navigate } from "../../../../common/navigate";
import { stringCompare } from "../../../../common/string/compare";
import { LocalizeFunc } from "../../../../common/translations/localize";
import {
DataTableColumnContainer,
RowClickedEvent,
@@ -26,9 +22,6 @@ import "../../../../components/ha-clickable-list-item";
import "../../../../components/ha-fab";
import "../../../../components/ha-icon";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-menu";
import type { HaMenu } from "../../../../components/ha-menu";
import "../../../../components/ha-menu-item";
import "../../../../components/ha-svg-icon";
import { LovelacePanelConfig } from "../../../../data/lovelace";
import {
@@ -48,11 +41,13 @@ import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-
import "../../../../layouts/hass-loading-screen";
import "../../../../layouts/hass-tabs-subpage-data-table";
import { HomeAssistant, Route } from "../../../../types";
import { LocalizeFunc } from "../../../../common/translations/localize";
import { getLovelaceStrategy } from "../../../lovelace/strategies/get-strategy";
import { showNewDashboardDialog } from "../../dashboard/show-dialog-new-dashboard";
import { lovelaceTabs } from "../ha-config-lovelace";
import { showDashboardConfigureStrategyDialog } from "./show-dialog-lovelace-dashboard-configure-strategy";
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
import { storage } from "../../../../common/decorators/storage";
type DataTableItem = Pick<
LovelaceDashboard,
@@ -75,14 +70,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
@state() private _dashboards: LovelaceDashboard[] = [];
@storage({
storage: "sessionStorage",
key: "lovelace-dashboards-table-search",
state: true,
subscribe: false,
})
private _filter: string = "";
@storage({
key: "lovelace-dashboards-table-sort",
state: false,
@@ -90,10 +77,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
})
private _activeSorting?: SortingChangedEvent;
@state() private _overflowDashboard?: LovelaceDashboard;
@query("#overflow-menu") private _overflowMenu!: HaMenu;
public willUpdate() {
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
@@ -219,36 +202,40 @@ export class HaConfigLovelaceDashboards extends LitElement {
};
}
columns.actions = {
columns.url_path = {
title: "",
width: "64px",
type: "icon-button",
template: (dashboard) => html`
<ha-icon-button
.dashboard=${dashboard}
.label=${this.hass.localize("ui.common.overflow_menu")}
.path=${mdiDotsVertical}
@click=${this._showOverflowMenu}
></ha-icon-button>
`,
label: localize(
"ui.panel.config.lovelace.dashboards.picker.headers.url"
),
filterable: true,
width: "100px",
template: (dashboard) =>
narrow
? html`
<ha-icon-button
.path=${mdiOpenInNew}
.urlPath=${dashboard.url_path}
@click=${this._navigate}
.label=${this.hass.localize(
"ui.panel.config.lovelace.dashboards.picker.open"
)}
></ha-icon-button>
`
: html`
<mwc-button
.urlPath=${dashboard.url_path}
@click=${this._navigate}
>${this.hass.localize(
"ui.panel.config.lovelace.dashboards.picker.open"
)}</mwc-button
>
`,
};
return columns;
}
);
private _showOverflowMenu = (ev) => {
if (
this._overflowMenu.open &&
ev.target === this._overflowMenu.anchorElement
) {
this._overflowMenu.close();
return;
}
this._overflowDashboard = ev.target.dashboard;
this._overflowMenu.anchorElement = ev.target;
this._overflowMenu.show();
};
private _getItems = memoize((dashboards: LovelaceDashboard[]) => {
const defaultMode = (
this.hass.panels?.lovelace?.config as LovelacePanelConfig
@@ -317,9 +304,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
.data=${this._getItems(this._dashboards)}
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
.filter=${this._filter}
@search-changed=${this._handleSearchChange}
@row-click=${this._navigate}
@row-click=${this._editDashboard}
id="url_path"
hasFab
clickable
@@ -351,22 +336,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</hass-tabs-subpage-data-table>
<ha-menu id="overflow-menu" positioning="fixed">
<ha-menu-item @click=${this._editDashboard}>
<ha-svg-icon .path=${mdiPencil} slot="start"></ha-svg-icon>
<div slot="headline">Edit</div>
</ha-menu-item>
<ha-menu-item>
<ha-svg-icon .path=${mdiStar} slot="start"></ha-svg-icon>
<div slot="headline">Set to default</div>
</ha-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item class="warning">
<ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon>
<div slot="headline">Delete</div>
</ha-menu-item>
</ha-menu>
`;
}
@@ -379,23 +348,21 @@ export class HaConfigLovelaceDashboards extends LitElement {
this._dashboards = await fetchDashboards(this.hass);
}
private _navigate(ev: CustomEvent) {
const urlPath = (ev.detail as RowClickedEvent).id;
navigate(`/${urlPath}`);
private _navigate(ev: Event) {
ev.stopPropagation();
navigate(`/${(ev.target as any).urlPath}`);
}
private _editDashboard = (ev) => {
ev.stopPropagation();
const dashboard = ev.currentTarget.parentElement.anchorElement.automation;
const urlPath = (ev.currentTarget as any).urlPath;
private _editDashboard(ev: CustomEvent) {
const urlPath = (ev.detail as RowClickedEvent).id;
if (urlPath === "energy") {
navigate("/config/energy");
return;
}
const dashboard = this._dashboards.find((res) => res.url_path === urlPath);
this._openDetailDialog(dashboard, urlPath);
};
}
private async _addDashboard() {
showNewDashboardDialog(this, {
@@ -486,10 +453,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
}
declare global {

View File

@@ -52,14 +52,6 @@ export class HaConfigLovelaceRescources extends LitElement {
@state() private _resources: LovelaceResource[] = [];
@storage({
storage: "sessionStorage",
key: "lovelace-resources-table-search",
state: true,
subscribe: false,
})
private _filter = "";
@storage({
key: "lovelace-resources-table-sort",
state: false,
@@ -146,8 +138,6 @@ export class HaConfigLovelaceRescources extends LitElement {
)}
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
.filter=${this._filter}
@search-changed=${this._handleSearchChange}
@row-click=${this._editResource}
hasFab
clickable
@@ -262,10 +252,6 @@ export class HaConfigLovelaceRescources extends LitElement {
this._activeSorting = ev.detail;
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@@ -8,7 +8,6 @@ import "../../../components/ha-formfield";
import "../../../components/ha-picture-upload";
import type { HaPictureUpload } from "../../../components/ha-picture-upload";
import "../../../components/ha-textfield";
import { adminChangeUsername } from "../../../data/auth";
import { PersonMutableParams } from "../../../data/person";
import {
deleteUser,
@@ -20,11 +19,10 @@ import {
import {
showAlertDialog,
showConfirmationDialog,
showPromptDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { ValueChangedEvent, HomeAssistant } from "../../../types";
import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant, ValueChangedEvent } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { showAddUserDialog } from "../users/show-dialog-add-user";
import { showAdminChangePasswordDialog } from "../users/show-dialog-admin-change-password";
@@ -138,9 +136,9 @@ class DialogPersonDetail extends LitElement {
></ha-picture-upload>
<ha-formfield
.label=${`${this.hass!.localize(
.label=${this.hass!.localize(
"ui.panel.config.person.detail.allow_login"
)}${this._user ? ` (${this._user.username})` : ""}`}
)}
>
<ha-switch
@change=${this._allowLoginChanged}
@@ -246,21 +244,13 @@ class DialogPersonDetail extends LitElement {
</mwc-button>
${this._user && this.hass.user?.is_owner
? html`<mwc-button
slot="secondaryAction"
@click=${this._changeUsername}
>
${this.hass.localize(
"ui.panel.config.users.editor.change_username"
)}
</mwc-button>
<mwc-button
slot="secondaryAction"
@click=${this._changePassword}
>
${this.hass.localize(
"ui.panel.config.users.editor.change_password"
)}
</mwc-button>`
slot="secondaryAction"
@click=${this._changePassword}
>
${this.hass.localize(
"ui.panel.config.users.editor.change_password"
)}
</mwc-button>`
: ""}
`
: nothing}
@@ -302,14 +292,11 @@ class DialogPersonDetail extends LitElement {
userAddedCallback: async (user?: User) => {
if (user) {
target.checked = true;
if (this._params!.entry) {
await this._params!.updateEntry({ user_id: user.id });
}
this._params?.refreshUsers();
this._user = user;
this._userId = user.id;
this._isAdmin = user.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
this._localOnly = user.local_only;
this._params?.refreshUsers();
}
},
name: this._name,
@@ -333,9 +320,6 @@ class DialogPersonDetail extends LitElement {
await deleteUser(this.hass, this._userId);
this._params?.refreshUsers();
this._userId = undefined;
this._user = undefined;
this._isAdmin = undefined;
this._localOnly = undefined;
}
}
@@ -365,53 +349,6 @@ class DialogPersonDetail extends LitElement {
showAdminChangePasswordDialog(this, { userId: this._user.id });
}
private async _changeUsername() {
if (!this._user) {
return;
}
const credential = this._user.credentials.find(
(cred) => cred.type === "homeassistant"
);
if (!credential) {
showAlertDialog(this, {
title: "No Home Assistant credentials found.",
});
return;
}
const newUsername = await showPromptDialog(this, {
inputLabel: this.hass.localize(
"ui.panel.config.users.change_username.new_username"
),
confirmText: this.hass.localize(
"ui.panel.config.users.change_username.change"
),
title: this.hass.localize(
"ui.panel.config.users.change_username.caption"
),
defaultValue: this._user.username!,
});
if (newUsername) {
try {
await adminChangeUsername(this.hass, this._user.id, newUsername);
this._params?.refreshUsers();
this._user = { ...this._user, username: newUsername };
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.users.change_username.username_changed"
),
});
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.users.change_username.failed"
),
text: err.message,
});
}
}
}
private async _updateEntry() {
this._submitting = true;
try {

View File

@@ -31,6 +31,8 @@ class DialogIntegrationStartup extends LitElement {
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
hideActions
.heading=${createCloseHeading(
this.hass,

View File

@@ -143,6 +143,8 @@ class DialogSystemInformation extends LitElement {
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.repairs.system_information")

View File

@@ -137,14 +137,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
@state() private _filteredScenes?: string[] | null;
@storage({
storage: "sessionStorage",
key: "scene-table-search",
state: true,
subscribe: false,
})
private _filter = "";
@storage({
storage: "sessionStorage",
key: "scene-table-filters-full",
@@ -551,8 +543,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
"ui.panel.config.scene.picker.no_scenes"
)}
@clear-filter=${this._clearFilter}
.filter=${this._filter}
@search-changed=${this._handleSearchChange}
hasFab
clickable
@row-click=${this._handleRowClicked}
@@ -1151,10 +1141,6 @@ ${rejected
this._activeCollapsed = ev.detail.value;
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@@ -141,14 +141,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
@state() private _filteredScripts?: string[] | null;
@storage({
storage: "sessionStorage",
key: "script-table-search",
state: true,
subscribe: false,
})
private _filter = "";
@storage({
storage: "sessionStorage",
key: "script-table-filters-full",
@@ -566,8 +558,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
"ui.panel.config.script.picker.no_scripts"
)}
@clear-filter=${this._clearFilter}
.filter=${this._filter}
@search-changed=${this._handleSearchChange}
hasFab
clickable
class=${this.narrow ? "narrow" : ""}
@@ -810,10 +800,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
`;
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
private _filterExpanded(ev) {
if (ev.detail.expanded) {
this._expandedFilter = ev.target.localName;

View File

@@ -35,7 +35,6 @@ import { documentationUrl } from "../../../util/documentation-url";
import { configSections } from "../ha-panel-config";
import { showTagDetailDialog } from "./show-dialog-tag-detail";
import "./tag-image";
import { storage } from "../../../common/decorators/storage";
export interface TagRowData extends Tag {
display_name: string;
@@ -58,14 +57,6 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
return this.hass.auth.external?.config.canWriteTag;
}
@storage({
storage: "sessionStorage",
key: "tags-table-search",
state: true,
subscribe: false,
})
private _filter = "";
private _columns = memoizeOne(
(narrow: boolean, _language, localize: LocalizeFunc) => {
const columns: DataTableColumnContainer<TagRowData> = {
@@ -198,8 +189,6 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
)}
.data=${this._data(this._tags)}
.noDataText=${this.hass.localize("ui.panel.config.tag.no_tags")}
.filter=${this._filter}
@search-changed=${this._handleSearchChange}
hasFab
>
<ha-icon-button
@@ -334,10 +323,6 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
return false;
}
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
}
declare global {

View File

@@ -10,16 +10,12 @@ import "../../../components/ha-label";
import "../../../components/ha-svg-icon";
import "../../../components/ha-switch";
import "../../../components/ha-textfield";
import { adminChangeUsername } from "../../../data/auth";
import {
computeUserBadges,
SYSTEM_GROUP_ID_ADMIN,
SYSTEM_GROUP_ID_USER,
} from "../../../data/user";
import {
showAlertDialog,
showPromptDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { showAdminChangePasswordDialog } from "./show-dialog-admin-change-password";
@@ -176,15 +172,11 @@ class DialogUserDetail extends LitElement {
`
: ""}
${!user.system_generated && this.hass.user?.is_owner
? html`<mwc-button @click=${this._changeUsername}>
${this.hass.localize(
"ui.panel.config.users.editor.change_username"
)} </mwc-button
><mwc-button @click=${this._changePassword}>
${this.hass.localize(
"ui.panel.config.users.editor.change_password"
)}
</mwc-button>`
? html`<mwc-button @click=${this._changePassword}>
${this.hass.localize(
"ui.panel.config.users.editor.change_password"
)}
</mwc-button>`
: ""}
</div>
@@ -258,56 +250,6 @@ class DialogUserDetail extends LitElement {
}
}
private async _changeUsername() {
const credential = this._params?.entry.credentials.find(
(cred) => cred.type === "homeassistant"
);
if (!credential) {
showAlertDialog(this, {
title: "No Home Assistant credentials found.",
});
return;
}
const newUsername = await showPromptDialog(this, {
inputLabel: this.hass.localize(
"ui.panel.config.users.change_username.new_username"
),
confirmText: this.hass.localize(
"ui.panel.config.users.change_username.change"
),
title: this.hass.localize(
"ui.panel.config.users.change_username.caption"
),
defaultValue: this._params!.entry.username!,
});
if (newUsername) {
try {
await adminChangeUsername(
this.hass,
this._params!.entry.id,
newUsername
);
this._params = {
...this._params!,
entry: { ...this._params!.entry, username: newUsername },
};
this._params.replaceEntry(this._params.entry);
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.users.change_username.username_changed"
),
});
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.users.change_username.failed"
),
text: err.message,
});
}
}
}
private async _changePassword() {
const credential = this._params?.entry.credentials.find(
(cred) => cred.type === "homeassistant"

View File

@@ -46,14 +46,6 @@ export class HaConfigUsers extends LitElement {
@storage({ key: "users-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string;
@storage({
storage: "sessionStorage",
key: "users-table-search",
state: true,
subscribe: false,
})
private _filter = "";
@storage({
key: "users-table-collapsed",
state: false,
@@ -182,7 +174,7 @@ export class HaConfigUsers extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
back-path="/config"
backPath="/config"
.tabs=${configSections.persons}
.columns=${this._columns(this.narrow, this.hass.localize)}
.data=${this._userData(this._users, this.hass.localize)}
@@ -192,8 +184,6 @@ export class HaConfigUsers extends LitElement {
@sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
.filter=${this._filter}
@search-changed=${this._handleSearchChange}
@row-click=${this._editUser}
hasFab
clickable
@@ -237,11 +227,6 @@ export class HaConfigUsers extends LitElement {
showUserDetailDialog(this, {
entry,
replaceEntry: (newEntry: User) => {
this._users = this._users!.map((ent) =>
ent.id === newEntry.id ? newEntry : ent
);
},
updateEntry: async (values) => {
const updated = await updateUser(this.hass!, entry!.id, values);
this._users = this._users!.map((ent) =>
@@ -298,10 +283,6 @@ export class HaConfigUsers extends LitElement {
private _handleCollapseChanged(ev: CustomEvent) {
this._activeCollapsed = ev.detail.value;
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
}
declare global {

View File

@@ -4,7 +4,6 @@ import { UpdateUserParams, User } from "../../../data/user";
export interface UserDetailDialogParams {
entry: User;
updateEntry: (updates: Partial<UpdateUserParams>) => Promise<unknown>;
replaceEntry: (entry: User) => void;
removeEntry: () => Promise<boolean>;
}

View File

@@ -80,13 +80,7 @@ export class VoiceAssistantsExpose extends LitElement {
@state() private _extEntities?: Record<string, ExtEntityRegistryEntry>;
@storage({
storage: "sessionStorage",
key: "voice-expose-table-search",
state: true,
subscribe: false,
})
private _filter = "";
@state() private _filter: string = history.state?.filter || "";
@state() private _searchParms = new URLSearchParams(window.location.search);
@@ -640,6 +634,7 @@ export class VoiceAssistantsExpose extends LitElement {
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
history.replaceState({ filter: this._filter }, "");
}
private _handleSelectionChanged(

View File

@@ -14,7 +14,7 @@ const SCHEMA = [
{
name: "location",
required: true,
selector: { location: { radius: true } },
selector: { location: { radius: true, radius_readonly: true } },
},
];
@@ -35,7 +35,6 @@ class DialogHomeZoneDetail extends LitElement {
this._data = {
latitude: this.hass.config.latitude,
longitude: this.hass.config.longitude,
radius: this.hass.config.radius,
};
}
@@ -74,6 +73,11 @@ class DialogHomeZoneDetail extends LitElement {
.computeLabel=${this._computeLabel}
@value-changed=${this._valueChanged}
></ha-form>
<p>
${this.hass!.localize(
"ui.panel.config.zone.detail.no_edit_home_zone_radius"
)}
</p>
</div>
<mwc-button
slot="primaryAction"
@@ -91,7 +95,7 @@ class DialogHomeZoneDetail extends LitElement {
location: {
latitude: data.latitude,
longitude: data.longitude,
radius: data.radius || 100,
radius: this.hass.states["zone.home"]?.attributes?.radius || 100,
},
}));
@@ -100,7 +104,6 @@ class DialogHomeZoneDetail extends LitElement {
const value = { ...ev.detail.value };
value.latitude = value.location.latitude;
value.longitude = value.location.longitude;
value.radius = value.location.radius;
delete value.location;
this._data = value;
}

View File

@@ -101,8 +101,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
: zoneRadiusColor,
location_editable:
entityState.entity_id === "zone.home" && this._canEditCore,
radius_editable:
entityState.entity_id === "zone.home" && this._canEditCore,
radius_editable: false,
})
);
const storageLocations: MarkerLocation[] = storageItems.map((zone) => ({
@@ -382,14 +381,8 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
});
}
private async _radiusUpdated(ev: CustomEvent) {
private _radiusUpdated(ev: CustomEvent) {
this._activeEntry = ev.detail.id;
if (ev.detail.id === "zone.home" && this._canEditCore) {
await saveCoreConfig(this.hass, {
radius: Math.round(ev.detail.radius),
});
return;
}
const entry = this._storageItems!.find((item) => item.id === ev.detail.id);
if (!entry) {
return;
@@ -485,7 +478,6 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
await saveCoreConfig(this.hass, {
latitude: values.latitude,
longitude: values.longitude,
radius: values.radius,
});
this._zoomZone("zone.home");
}

View File

@@ -282,7 +282,6 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
{ statistic_id: issue.data.statistic_id }
)}`,
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: async () => {
await clearStatistics(this.hass, [issue.data.statistic_id]);
this._deletedStatistics.add(issue.data.statistic_id);
@@ -315,7 +314,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
});
break;
case "entity_no_longer_recorded":
showConfirmationDialog(this, {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.title"
),
@@ -336,17 +335,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_3_link"
)}</a
><br /><br />
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_4"
)}`,
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: async () => {
await clearStatistics(this.hass, [issue.data.statistic_id]);
this._deletedStatistics.add(issue.data.statistic_id);
this._validateStatistics();
},
>`,
});
break;
case "unsupported_state_class":
@@ -392,7 +381,6 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
{ statistic_id: issue.data.statistic_id }
)}`,
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: async () => {
await clearStatistics(this.hass, [issue.data.statistic_id]);
this._deletedStatistics.add(issue.data.statistic_id);

View File

@@ -6,6 +6,7 @@ import {
} from "@mdi/js";
import { ActionDetail } from "@material/mwc-list";
import { differenceInHours } from "date-fns";
import { HassEntity } from "home-assistant-js-websocket";
import {
HassServiceTarget,
UnsubscribeFunc,
@@ -45,7 +46,11 @@ import {
computeHistory,
subscribeHistory,
} from "../../data/history";
import { Statistics, fetchStatistics } from "../../data/recorder";
import {
fetchStatistics,
Statistics,
getRecordedExcludedEntities,
} from "../../data/recorder";
import {
expandAreaTarget,
expandDeviceTarget,
@@ -95,6 +100,8 @@ class HaPanelHistory extends LitElement {
private _interval?: number;
private _excludedEntities?: string[];
public constructor() {
super();
@@ -185,6 +192,7 @@ class HaPanelHistory extends LitElement {
.hass=${this.hass}
.value=${this._targetPickerValue}
.disabled=${this._isLoading}
.entityFilter=${this._entityFilter}
addOnTop
@value-changed=${this._targetsChanged}
></ha-target-picker>
@@ -211,6 +219,10 @@ class HaPanelHistory extends LitElement {
`;
}
private _entityFilter = (entity: HassEntity): boolean =>
!this._excludedEntities ||
!this._excludedEntities.includes(entity.entity_id);
private mergeHistoryResults(
ltsResult: HistoryResult,
historyResult: HistoryResult
@@ -362,8 +374,17 @@ class HaPanelHistory extends LitElement {
}
}
private async _getRecordedExcludedEntities() {
const { recorded_ids: _recordedIds, excluded_ids: excludedIds } =
await getRecordedExcludedEntities(this.hass);
this._excludedEntities = excludedIds;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._getRecordedExcludedEntities();
const searchParams = extractSearchParamsObject();
if (searchParams.back === "1" && history.length > 1) {
this._showBack = true;

View File

@@ -1,7 +1,7 @@
export const filterModes = <T extends string = string>(
supportedModes: T[] | undefined,
selectedModes: T[] | undefined
): T[] =>
export const filterModes = (
supportedModes: string[] | undefined,
selectedModes: string[] | undefined
): string[] =>
selectedModes
? selectedModes.filter((mode) => (supportedModes || []).includes(mode))
: supportedModes || [];

View File

@@ -6,6 +6,7 @@ import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateColorCss } from "../../../common/entity/state_color";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-control-select";
@@ -69,18 +70,37 @@ class HuiAlarmModeCardFeature
}
}
private _getCurrentMode = memoizeOne((stateObj: AlarmControlPanelEntity) => {
const supportedModes = supportedAlarmModes(stateObj);
return supportedModes.find((mode) => mode === stateObj.state);
});
private _modes = memoizeOne(
(
stateObj: AlarmControlPanelEntity,
selectedModes: AlarmMode[] | undefined
) => {
if (!selectedModes) {
return [];
}
return (Object.keys(ALARM_MODES) as AlarmMode[]).filter((mode) => {
const feature = ALARM_MODES[mode].feature;
return (
(!feature || supportsFeature(stateObj, feature)) &&
selectedModes.includes(mode)
);
});
}
);
private _getCurrentMode(stateObj: AlarmControlPanelEntity) {
return this._modes(stateObj, this._config?.modes).find(
(mode) => mode === stateObj.state
);
}
private async _valueChanged(ev: CustomEvent) {
if (!this.stateObj) return;
const mode = (ev.detail as any).value as AlarmMode;
if (mode === this.stateObj.state) return;
if (mode === this.stateObj!.state) return;
const oldMode = this._getCurrentMode(this.stateObj);
const oldMode = this._getCurrentMode(this.stateObj!);
this._currentMode = mode;
try {
@@ -133,7 +153,6 @@ class HuiAlarmModeCardFeature
</ha-control-button-group>
`;
}
return html`
<div class="container">
<ha-control-select

View File

@@ -24,7 +24,7 @@ import { calculateStatisticsSumGrowth } from "../../../../data/recorder";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../../types";
import type { LovelaceCard } from "../../types";
import type { EnergyGridNeutralityGaugeCardConfig } from "../types";
import type { EnergyGridGaugeCardConfig } from "../types";
import { hasConfigChanged } from "../../common/has-changed";
const LEVELS: LevelDefinition[] = [
@@ -39,7 +39,7 @@ class HuiEnergyGridGaugeCard
{
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: EnergyGridNeutralityGaugeCardConfig;
@state() private _config?: EnergyGridGaugeCardConfig;
@state() private _data?: EnergyData;
@@ -59,7 +59,7 @@ class HuiEnergyGridGaugeCard
return 4;
}
public setConfig(config: EnergyGridNeutralityGaugeCardConfig): void {
public setConfig(config: EnergyGridGaugeCardConfig): void {
this._config = config;
}

View File

@@ -1,6 +1,5 @@
import { PropertyValueMap, PropertyValues, ReactiveElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { PropertyValues, ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { MediaQueriesListener } from "../../../common/dom/media_query";
import "../../../components/ha-svg-icon";
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
@@ -11,41 +10,15 @@ import {
checkConditionsMet,
} from "../common/validate-condition";
import { createCardElement } from "../create-element/create-card-element";
import { createErrorCardConfig } from "../create-element/create-element-base";
import type { LovelaceCard, LovelaceLayoutOptions } from "../types";
declare global {
interface HASSDomEvents {
"card-visibility-changed": { value: boolean };
"card-updated": undefined;
}
}
import type { Lovelace, LovelaceCard, LovelaceLayoutOptions } from "../types";
@customElement("hui-card")
export class HuiCard extends ReactiveElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public preview = false;
@property({ attribute: false }) public lovelace!: Lovelace;
@property({ type: Boolean }) public isPanel = false;
set config(config: LovelaceCardConfig | undefined) {
if (!config) return;
if (config.type !== this._config?.type) {
this._buildElement(config);
} else if (config !== this.config) {
this._element?.setConfig(config);
fireEvent(this, "card-updated");
}
this._config = config;
}
@property({ attribute: false })
public get config() {
return this._config;
}
private _config?: LovelaceCardConfig;
@state() public _config?: LovelaceCardConfig;
private _element?: LovelaceCard;
@@ -63,7 +36,7 @@ export class HuiCard extends ReactiveElement {
public connectedCallback() {
super.connectedCallback();
this._listenMediaQueries();
this._updateVisibility();
this._updateElement();
}
public getCardSize(): number | Promise<number> {
@@ -75,7 +48,7 @@ export class HuiCard extends ReactiveElement {
}
public getLayoutOptions(): LovelaceLayoutOptions {
const configOptions = this.config?.layout_options ?? {};
const configOptions = this._config?.layout_options ?? {};
if (this._element) {
const cardOptions = this._element.getLayoutOptions?.() ?? {};
return {
@@ -86,84 +59,37 @@ export class HuiCard extends ReactiveElement {
return configOptions;
}
public getElementLayoutOptions(): LovelaceLayoutOptions {
return this._element?.getLayoutOptions?.() ?? {};
}
private _createElement(config: LovelaceCardConfig) {
const element = createCardElement(config);
element.hass = this.hass;
element.preview = this.preview;
// For backwards compatibility
(element as any).editMode = this.preview;
// Update element when the visibility of the card changes (e.g. conditional card or filter card)
element.addEventListener("card-visibility-changed", (ev: Event) => {
ev.stopPropagation();
this._updateVisibility();
});
element.addEventListener(
"ll-upgrade",
(ev: Event) => {
ev.stopPropagation();
fireEvent(this, "card-updated");
},
{ once: true }
);
element.addEventListener(
"ll-rebuild",
(ev: Event) => {
ev.stopPropagation();
this._buildElement(config);
fireEvent(this, "card-updated");
},
{ once: true }
);
return element;
}
private _buildElement(config: LovelaceCardConfig) {
this._element = this._createElement(config);
public setConfig(config: LovelaceCardConfig): void {
if (this._config === config) {
return;
}
this._config = config;
this._element = createCardElement(config);
this._element.hass = this.hass;
this._element.editMode = this.lovelace.editMode;
while (this.lastChild) {
this.removeChild(this.lastChild);
}
this._updateVisibility();
this.appendChild(this._element!);
}
protected update(changedProps: PropertyValues<typeof this>) {
super.update(changedProps);
protected update(changedProperties: PropertyValues<typeof this>) {
super.update(changedProperties);
if (this._element) {
if (changedProps.has("hass")) {
try {
this._element.hass = this.hass;
} catch (e: any) {
this._buildElement(createErrorCardConfig(e.message, null));
}
if (changedProperties.has("hass")) {
this._element.hass = this.hass;
}
if (changedProps.has("preview")) {
try {
this._element.preview = this.preview;
// For backwards compatibility
(this._element as any).editMode = this.preview;
} catch (e: any) {
this._buildElement(createErrorCardConfig(e.message, null));
}
if (changedProperties.has("lovelace")) {
this._element.editMode = this.lovelace.editMode;
}
if (changedProps.has("isPanel")) {
this._element.isPanel = this.isPanel;
if (changedProperties.has("hass") || changedProperties.has("lovelace")) {
this._updateElement();
}
}
}
protected willUpdate(
changedProps: PropertyValueMap<any> | Map<PropertyKey, unknown>
): void {
if (changedProps.has("hass") || changedProps.has("preview")) {
this._updateVisibility();
}
}
private _clearMediaQueries() {
this._listeners.forEach((unsub) => unsub());
this._listeners = [];
@@ -171,50 +97,35 @@ export class HuiCard extends ReactiveElement {
private _listenMediaQueries() {
this._clearMediaQueries();
if (!this.config?.visibility) {
if (!this._config?.visibility) {
return;
}
const conditions = this.config.visibility;
const conditions = this._config.visibility;
const hasOnlyMediaQuery =
conditions.length === 1 &&
conditions[0].condition === "screen" &&
!!conditions[0].media_query;
this._listeners = attachConditionMediaQueriesListeners(
this.config.visibility,
this._config.visibility,
(matches) => {
this._updateVisibility(hasOnlyMediaQuery && matches);
this._updateElement(hasOnlyMediaQuery && matches);
}
);
}
private _updateVisibility(forceVisible?: boolean) {
if (!this._element || !this.hass) {
private _updateElement(forceVisible?: boolean) {
if (!this._element) {
return;
}
if (this._element.hidden) {
this._setElementVisibility(false);
return;
}
const visible =
forceVisible ||
this.preview ||
!this.config?.visibility ||
checkConditionsMet(this.config.visibility, this.hass);
this._setElementVisibility(visible);
}
private _setElementVisibility(visible: boolean) {
if (!this._element) return;
if (this.hidden !== !visible) {
this.style.setProperty("display", visible ? "" : "none");
this.toggleAttribute("hidden", !visible);
fireEvent(this, "card-visibility-changed", { value: visible });
}
this.lovelace.editMode ||
!this._config?.visibility ||
checkConditionsMet(this._config.visibility, this.hass);
this.style.setProperty("display", visible ? "" : "none");
this.toggleAttribute("hidden", !visible);
if (!visible && this._element.parentElement) {
this.removeChild(this._element);
} else if (visible && !this._element.parentElement) {

View File

@@ -1,8 +1,8 @@
import { customElement } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import { computeCardSize } from "../common/compute-card-size";
import { HuiConditionalBase } from "../components/hui-conditional-base";
import { createCardElement } from "../create-element/create-card-element";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { ConditionalCardConfig } from "./types";
@@ -37,19 +37,25 @@ class HuiConditionalCard extends HuiConditionalBase implements LovelaceCard {
}
private _createCardElement(cardConfig: LovelaceCardConfig) {
const element = document.createElement("hui-card");
element.hass = this.hass;
element.preview = this.preview;
element.config = cardConfig;
const element = createCardElement(cardConfig) as LovelaceCard;
if (this.hass) {
element.hass = this.hass;
}
element.addEventListener(
"ll-rebuild",
(ev) => {
ev.stopPropagation();
this._rebuildCard(cardConfig);
},
{ once: true }
);
return element;
}
protected setVisibility(conditionMet: boolean): void {
const visible = this.preview || conditionMet;
const previouslyHidden = this.hidden;
super.setVisibility(conditionMet);
if (previouslyHidden !== this.hidden) {
fireEvent(this, "card-visibility-changed", { value: visible });
private _rebuildCard(config: LovelaceCardConfig): void {
this._element = this._createCardElement(config);
if (this.lastChild) {
this.replaceChild(this._element, this.lastChild);
}
}
}

View File

@@ -1,6 +1,5 @@
import { PropertyValues, ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import { HomeAssistant } from "../../../types";
import { computeCardSize } from "../common/compute-card-size";
@@ -12,9 +11,9 @@ import {
checkConditionsMet,
extractConditionEntityIds,
} from "../common/validate-condition";
import { createCardElement } from "../create-element/create-card-element";
import { EntityFilterEntityConfig } from "../entity-rows/types";
import { LovelaceCard } from "../types";
import { HuiCard } from "./hui-card";
import { EntityFilterCardConfig } from "./types";
@customElement("hui-entity-filter-card")
@@ -55,11 +54,11 @@ export class HuiEntityFilterCard
@property({ type: Boolean }) public isPanel = false;
@property({ type: Boolean }) public preview = false;
@property({ type: Boolean }) public editMode = false;
@state() private _config?: EntityFilterCardConfig;
private _element?: HuiCard;
private _element?: LovelaceCard;
private _configEntities?: EntityFilterEntityConfig[];
@@ -117,7 +116,7 @@ export class HuiEntityFilterCard
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (this._element) {
this._element.hass = this.hass;
this._element.preview = this.preview;
this._element.editMode = this.editMode;
this._element.isPanel = this.isPanel;
}
@@ -164,21 +163,18 @@ export class HuiEntityFilterCard
});
if (entitiesList.length === 0 && this._config.show_empty === false) {
if (!this.hidden) {
this.style.display = "none";
this.toggleAttribute("hidden", true);
fireEvent(this, "card-visibility-changed", { value: false });
}
this.style.display = "none";
this.toggleAttribute("hidden", true);
return;
}
if (!this.lastChild) {
this._element.config = {
this._element.setConfig({
...this._baseCardConfig!,
entities: entitiesList,
};
});
this._oldEntities = entitiesList;
} else {
} else if (this._element.tagName !== "HUI-ERROR-CARD") {
const isSame =
this._oldEntities &&
entitiesList.length === this._oldEntities.length &&
@@ -186,10 +182,10 @@ export class HuiEntityFilterCard
if (!isSame) {
this._oldEntities = entitiesList;
this._element.config = {
this._element.setConfig({
...this._baseCardConfig!,
entities: entitiesList,
};
});
}
}
@@ -198,11 +194,8 @@ export class HuiEntityFilterCard
this.appendChild(this._element);
}
if (this.hidden) {
this.style.display = "block";
this.toggleAttribute("hidden", false);
fireEvent(this, "card-visibility-changed", { value: true });
}
this.style.display = "block";
this.toggleAttribute("hidden", false);
}
private _haveEntitiesChanged(oldHass: HomeAssistant | null): boolean {
@@ -245,12 +238,33 @@ export class HuiEntityFilterCard
}
private _createCardElement(cardConfig: LovelaceCardConfig) {
const element = document.createElement("hui-card");
element.hass = this.hass;
element.preview = this.preview;
element.config = cardConfig;
const element = createCardElement(cardConfig) as LovelaceCard;
if (this.hass) {
element.hass = this.hass;
}
element.isPanel = this.isPanel;
element.editMode = this.editMode;
element.addEventListener(
"ll-rebuild",
(ev) => {
ev.stopPropagation();
this._rebuildCard(element, cardConfig);
},
{ once: true }
);
return element;
}
private _rebuildCard(
cardElToReplace: LovelaceCard,
config: LovelaceCardConfig
): void {
const newCardEl = this._createCardElement(config);
if (cardElToReplace.parentElement) {
cardElToReplace.parentElement!.replaceChild(newCardEl, cardElToReplace);
}
this._element = newCardEl;
}
}
declare global {

View File

@@ -1,6 +1,6 @@
import { dump } from "js-yaml";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, state } from "lit/decorators";
import "../../../components/ha-alert";
import { HomeAssistant } from "../../../types";
import { LovelaceCard } from "../types";
@@ -10,8 +10,6 @@ import { ErrorCardConfig } from "./types";
export class HuiErrorCard extends LitElement implements LovelaceCard {
public hass?: HomeAssistant;
@property({ attribute: false }) public preview = false;
@state() private _config?: ErrorCardConfig;
public getCardSize(): number {

View File

@@ -38,7 +38,7 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public preview = false;
@property({ type: Boolean }) public editMode = false;
@state() private _config?: MarkdownCardConfig;
@@ -163,12 +163,12 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
user: this.hass.user!.name,
},
strict: true,
report_errors: this.preview,
report_errors: this.editMode,
}
);
await this._unsubRenderTemplate;
} catch (e: any) {
if (this.preview) {
if (this.editMode) {
this._error = e.message;
this._errorLevel = undefined;
}

View File

@@ -1,12 +1,19 @@
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
nothing,
} from "lit";
import { property, state } from "lit/decorators";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
import { fireEvent } from "../../../common/dom/fire_event";
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import { HomeAssistant } from "../../../types";
import { createCardElement } from "../create-element/create-card-element";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import "./hui-card";
import type { HuiCard } from "./hui-card";
import { StackCardConfig } from "./types";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
extends LitElement
@@ -23,9 +30,9 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public preview = false;
@property({ type: Boolean }) public editMode = false;
@state() protected _cards?: HuiCard[];
@state() protected _cards?: LovelaceCard[];
@state() protected _config?: T;
@@ -42,36 +49,30 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
}
this._config = config;
this._cards = config.cards.map((card) => {
const element = this._createCardElement(card);
const element = this._createCardElement(card) as LovelaceCard;
return element;
});
}
protected update(changedProperties) {
super.update(changedProperties);
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (
!this._cards ||
(!changedProps.has("hass") && !changedProps.has("editMode"))
) {
return;
}
if (this._cards) {
if (changedProperties.has("hass")) {
this._cards.forEach((card) => {
card.hass = this.hass;
});
for (const element of this._cards) {
if (this.hass) {
element.hass = this.hass;
}
if (changedProperties.has("editMode")) {
this._cards.forEach((card) => {
card.preview = this.preview;
});
if (this.editMode !== undefined) {
element.editMode = this.editMode;
}
}
}
private _createCardElement(cardConfig: LovelaceCardConfig) {
const element = document.createElement("hui-card");
element.hass = this.hass;
element.preview = this.preview;
element.config = cardConfig;
return element;
}
protected render() {
if (!this._config || !this._cards) {
return nothing;
@@ -109,4 +110,34 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
}
`;
}
private _createCardElement(cardConfig: LovelaceCardConfig) {
const element = createCardElement(cardConfig) as LovelaceCard;
if (this.hass) {
element.hass = this.hass;
}
element.addEventListener(
"ll-rebuild",
(ev) => {
ev.stopPropagation();
this._rebuildCard(element, cardConfig);
fireEvent(this, "ll-rebuild");
},
{ once: true }
);
return element;
}
private _rebuildCard(
cardElToReplace: LovelaceCard,
config: LovelaceCardConfig
): void {
const newCardEl = this._createCardElement(config);
if (cardElToReplace.parentElement) {
cardElToReplace.parentElement.replaceChild(newCardEl, cardElToReplace);
}
this._cards = this._cards!.map((curCardEl) =>
curCardEl === cardElToReplace ? newCardEl : curCardEl
);
}
}

View File

@@ -26,7 +26,6 @@ import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import "../../../components/ha-card";
import "../../../components/ha-check-list-item";
import "../../../components/ha-checkbox";
@@ -105,7 +104,6 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
disconnectedCallback(): void {
super.disconnectedCallback();
this._unsubItems?.then((unsub) => unsub());
this._unsubItems = undefined;
}
public getCardSize(): number {
@@ -244,7 +242,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
${this.todoListSupportsFeature(
TodoListEntityFeature.MOVE_TODO_ITEM
)
? html`<ha-button-menu @closed=${stopPropagation}>
? html`<ha-button-menu>
<ha-icon-button
slot="trigger"
.path=${mdiDotsVertical}
@@ -288,7 +286,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
${this.todoListSupportsFeature(
TodoListEntityFeature.DELETE_TODO_ITEM
)
? html`<ha-button-menu @closed=${stopPropagation}>
? html`<ha-button-menu>
<ha-icon-button
slot="trigger"
.path=${mdiDotsVertical}

View File

@@ -118,74 +118,85 @@ export interface EnergyCardBaseConfig extends LovelaceCardConfig {
collection_key?: string;
}
export interface EnergySummaryCardConfig extends EnergyCardBaseConfig {
export interface EnergySummaryCardConfig extends LovelaceCardConfig {
type: "energy-summary";
title?: string;
collection_key?: string;
}
export interface EnergyDistributionCardConfig extends EnergyCardBaseConfig {
export interface EnergyDistributionCardConfig extends LovelaceCardConfig {
type: "energy-distribution";
title?: string;
link_dashboard?: boolean;
collection_key?: string;
}
export interface EnergyUsageGraphCardConfig extends EnergyCardBaseConfig {
export interface EnergyUsageGraphCardConfig extends LovelaceCardConfig {
type: "energy-usage-graph";
title?: string;
collection_key?: string;
}
export interface EnergySolarGraphCardConfig extends EnergyCardBaseConfig {
export interface EnergySolarGraphCardConfig extends LovelaceCardConfig {
type: "energy-solar-graph";
title?: string;
collection_key?: string;
}
export interface EnergyGasGraphCardConfig extends EnergyCardBaseConfig {
export interface EnergyGasGraphCardConfig extends LovelaceCardConfig {
type: "energy-gas-graph";
title?: string;
collection_key?: string;
}
export interface EnergyWaterGraphCardConfig extends EnergyCardBaseConfig {
export interface EnergyWaterGraphCardConfig extends LovelaceCardConfig {
type: "energy-water-graph";
title?: string;
collection_key?: string;
}
export interface EnergyDevicesGraphCardConfig extends EnergyCardBaseConfig {
export interface EnergyDevicesGraphCardConfig extends LovelaceCardConfig {
type: "energy-devices-graph";
title?: string;
collection_key?: string;
max_devices?: number;
}
export interface EnergyDevicesDetailGraphCardConfig
extends EnergyCardBaseConfig {
export interface EnergyDevicesDetailGraphCardConfig extends LovelaceCardConfig {
type: "energy-devices-detail-graph";
title?: string;
collection_key?: string;
max_devices?: number;
}
export interface EnergySourcesTableCardConfig extends EnergyCardBaseConfig {
export interface EnergySourcesTableCardConfig extends LovelaceCardConfig {
type: "energy-sources-table";
title?: string;
collection_key?: string;
}
export interface EnergySolarGaugeCardConfig extends EnergyCardBaseConfig {
export interface EnergySolarGaugeCardConfig extends LovelaceCardConfig {
type: "energy-solar-consumed-gauge";
title?: string;
collection_key?: string;
}
export interface EnergySelfSufficiencyGaugeCardConfig
extends EnergyCardBaseConfig {
extends LovelaceCardConfig {
type: "energy-self-sufficiency-gauge";
title?: string;
collection_key?: string;
}
export interface EnergyGridNeutralityGaugeCardConfig
extends EnergyCardBaseConfig {
type: "energy-grid-neutrality-gauge";
export interface EnergyGridGaugeCardConfig extends LovelaceCardConfig {
type: "energy-grid-result-gauge";
title?: string;
collection_key?: string;
}
export interface EnergyCarbonGaugeCardConfig extends EnergyCardBaseConfig {
export interface EnergyCarbonGaugeCardConfig extends LovelaceCardConfig {
type: "energy-carbon-consumed-gauge";
title?: string;
collection_key?: string;
}
export interface EntityFilterCardConfig extends LovelaceCardConfig {

View File

@@ -1,5 +1,5 @@
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
import { SENSOR_ENTITIES, ASSIST_ENTITIES } from "../../../common/const";
import { SENSOR_ENTITIES } from "../../../common/const";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
@@ -35,16 +35,17 @@ import { ButtonsHeaderFooterConfig } from "../header-footer/types";
const HIDE_DOMAIN = new Set([
"automation",
"configurator",
"conversation",
"device_tracker",
"event",
"geo_location",
"notify",
"persistent_notification",
"script",
"sun",
"todo",
"zone",
...ASSIST_ENTITIES,
"event",
"tts",
"stt",
"todo",
]);
const HIDE_PLATFORM = new Set(["mobile_app"]);

View File

@@ -353,7 +353,7 @@ export class HuiCardOptions extends LitElement {
allowDashboardChange: true,
header: this.hass!.localize("ui.panel.lovelace.editor.move_card.header"),
viewSelectedCallback: async (urlPath, selectedDashConfig, viewIndex) => {
const view = selectedDashConfig.views[viewIndex];
const view = this.lovelace!.config.views[viewIndex];
if (!isStrategyView(view) && view.type === SECTION_VIEW_LAYOUT) {
showAlertDialog(this, {

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