mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-24 16:49:27 +00:00
Compare commits
55 Commits
heading-en
...
20241010.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1d88c4465b | ||
![]() |
af2d575bf0 | ||
![]() |
92165d776a | ||
![]() |
a8bbd8ab90 | ||
![]() |
43ac9dbea7 | ||
![]() |
bba9eca4e9 | ||
![]() |
40f65b1980 | ||
![]() |
23a33b10a1 | ||
![]() |
67a93013c7 | ||
![]() |
1f838d7529 | ||
![]() |
ffc0435144 | ||
![]() |
5877d69c87 | ||
![]() |
99035cea8f | ||
![]() |
1b441a7eec | ||
![]() |
ad49e9f7b0 | ||
![]() |
e32b15ede2 | ||
![]() |
a35b4376ea | ||
![]() |
619f9f76ee | ||
![]() |
f771bc10db | ||
![]() |
b8889a1183 | ||
![]() |
eb6b45eaed | ||
![]() |
31a748ed93 | ||
![]() |
0110bdd24a | ||
![]() |
365b712976 | ||
![]() |
7d97dbe15b | ||
![]() |
8bc0ea5a0b | ||
![]() |
44948a3474 | ||
![]() |
bc51b53b4a | ||
![]() |
67217b9dd0 | ||
![]() |
487795b7c4 | ||
![]() |
a30e0d33f9 | ||
![]() |
0c1b8abe03 | ||
![]() |
ce9c5149d5 | ||
![]() |
adbcdc62eb | ||
![]() |
faf872bfb8 | ||
![]() |
fe0fb2382a | ||
![]() |
cdd29295e5 | ||
![]() |
f7532f3476 | ||
![]() |
c8930cec87 | ||
![]() |
f9c336890d | ||
![]() |
c721de109f | ||
![]() |
1c95e8d6ec | ||
![]() |
57cf2c1341 | ||
![]() |
f7d5c5f850 | ||
![]() |
470f5127f4 | ||
![]() |
34e361601a | ||
![]() |
70d6cce8f8 | ||
![]() |
f9814f35d1 | ||
![]() |
f30603753e | ||
![]() |
ce9993fd36 | ||
![]() |
4c2044e70a | ||
![]() |
7f96c1fbe1 | ||
![]() |
75e24780c1 | ||
![]() |
95580bc4c0 | ||
![]() |
175f68e0cf |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||
- name: Setup lint cache
|
||||
uses: actions/cache@v4.0.2
|
||||
uses: actions/cache@v4.1.0
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
|
@@ -27,3 +27,5 @@ A complete guide can be found at the following [link](https://www.home-assistant
|
||||
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.
|
||||
|
||||
We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variety of devices.
|
||||
|
||||
[](https://www.openhomefoundation.org/)
|
||||
|
@@ -111,6 +111,16 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
|
||||
friendly_name: "Living room Temperature",
|
||||
},
|
||||
},
|
||||
"sensor.living_room_humidity": {
|
||||
entity_id: "sensor.living_room_humidity",
|
||||
state: "57",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
unit_of_measurement: "%",
|
||||
device_class: "humidity",
|
||||
friendly_name: "Living room Humidity",
|
||||
},
|
||||
},
|
||||
"sensor.outdoor_temperature": {
|
||||
entity_id: "sensor.outdoor_temperature",
|
||||
state: "10.5",
|
||||
@@ -189,6 +199,14 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
|
||||
supported_features: 32,
|
||||
},
|
||||
},
|
||||
"binary_sensor.kitchen_motion": {
|
||||
entity_id: "light.kitchen_motion",
|
||||
state: "on",
|
||||
attributes: {
|
||||
device_class: "motion",
|
||||
friendly_name: "Kitchen motion",
|
||||
},
|
||||
},
|
||||
"light.worktop_spotlights": {
|
||||
entity_id: "light.worktop_spotlights",
|
||||
state: "off",
|
||||
@@ -423,6 +441,14 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
|
||||
supported_features: 64063,
|
||||
},
|
||||
},
|
||||
"switch.in_meeting": {
|
||||
entity_id: "switch.in_meeting",
|
||||
state: "on",
|
||||
attributes: {
|
||||
icon: "mdi:laptop-account",
|
||||
friendly_name: "In a meeting",
|
||||
},
|
||||
},
|
||||
"sensor.standing_desk_height": {
|
||||
entity_id: "sensor.standing_desk_height",
|
||||
state: "72",
|
||||
|
@@ -30,12 +30,36 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
|
||||
? []
|
||||
: [
|
||||
{
|
||||
title: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`,
|
||||
cards: [{ type: "custom:ha-demo-card" }],
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`,
|
||||
},
|
||||
{ type: "custom:ha-demo-card" },
|
||||
],
|
||||
},
|
||||
]),
|
||||
{
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: localize(
|
||||
"ui.panel.page-demo.config.sections.titles.living_room"
|
||||
),
|
||||
icon: "mdi:sofa",
|
||||
badges: [
|
||||
{
|
||||
type: "entity",
|
||||
entity: "sensor.living_room_temperature",
|
||||
color: "red",
|
||||
},
|
||||
{
|
||||
type: "entity",
|
||||
entity: "sensor.living_room_humidity",
|
||||
color: "indigo",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "light.floor_lamp",
|
||||
@@ -54,13 +78,6 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
|
||||
type: "tile",
|
||||
entity: "light.bar_lamp",
|
||||
},
|
||||
{
|
||||
graph: "line",
|
||||
type: "sensor",
|
||||
entity: "sensor.living_room_temperature",
|
||||
detail: 1,
|
||||
name: "Temperature",
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "cover.living_room_garden_shutter",
|
||||
@@ -71,11 +88,25 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
|
||||
entity: "media_player.living_room_nest_mini",
|
||||
},
|
||||
],
|
||||
title: `🛋️ ${localize("ui.panel.page-demo.config.sections.titles.living_room")} `,
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: localize(
|
||||
"ui.panel.page-demo.config.sections.titles.kitchen"
|
||||
),
|
||||
icon: "mdi:fridge",
|
||||
badges: [
|
||||
{
|
||||
type: "entity",
|
||||
entity: "binary_sensor.kitchen_motion",
|
||||
show_state: false,
|
||||
color: "blue",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "cover.kitchen_shutter",
|
||||
@@ -106,11 +137,17 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
|
||||
entity: "media_player.kitchen_nest_audio",
|
||||
},
|
||||
],
|
||||
title: `👩🍳 ${localize("ui.panel.page-demo.config.sections.titles.kitchen")}`,
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: localize(
|
||||
"ui.panel.page-demo.config.sections.titles.energy"
|
||||
),
|
||||
icon: "mdi:transmission-tower",
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "binary_sensor.tesla_wall_connector_vehicle_connected",
|
||||
@@ -148,11 +185,17 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
|
||||
color: "dark-grey",
|
||||
},
|
||||
],
|
||||
title: `⚡️ ${localize("ui.panel.page-demo.config.sections.titles.energy")}`,
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: localize(
|
||||
"ui.panel.page-demo.config.sections.titles.climate"
|
||||
),
|
||||
icon: "mdi:thermometer",
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "sun.sun",
|
||||
@@ -185,16 +228,38 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
|
||||
state_content: ["preset_mode", "current_temperature"],
|
||||
},
|
||||
],
|
||||
title: `🌤️ ${localize("ui.panel.page-demo.config.sections.titles.climate")}`,
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: localize(
|
||||
"ui.panel.page-demo.config.sections.titles.study"
|
||||
),
|
||||
icon: "mdi:desk-lamp",
|
||||
badges: [
|
||||
{
|
||||
type: "entity",
|
||||
entity: "switch.in_meeting",
|
||||
state: "on",
|
||||
state_content: "name",
|
||||
visibility: [
|
||||
{
|
||||
condition: "state",
|
||||
state: "on",
|
||||
entity: "switch.in_meeting",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "cover.study_shutter",
|
||||
name: "Shutter",
|
||||
},
|
||||
|
||||
{
|
||||
type: "tile",
|
||||
entity: "light.study_spotlights",
|
||||
@@ -211,12 +276,23 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
|
||||
color: "brown",
|
||||
icon: "mdi:desk",
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "switch.in_meeting",
|
||||
name: "Meeting mode",
|
||||
},
|
||||
],
|
||||
title: `🧑💻 ${localize("ui.panel.page-demo.config.sections.titles.study")}`,
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: localize(
|
||||
"ui.panel.page-demo.config.sections.titles.outdoor"
|
||||
),
|
||||
icon: "mdi:tree",
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "light.outdoor_light",
|
||||
@@ -246,11 +322,17 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
|
||||
name: "Illuminance",
|
||||
},
|
||||
],
|
||||
title: `🌳 ${localize("ui.panel.page-demo.config.sections.titles.outdoor")}`,
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: localize(
|
||||
"ui.panel.page-demo.config.sections.titles.updates"
|
||||
),
|
||||
icon: "mdi:update",
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "automation.home_assistant_auto_update",
|
||||
@@ -276,7 +358,6 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
|
||||
icon: "mdi:home-assistant",
|
||||
},
|
||||
],
|
||||
title: `🎉 ${localize("ui.panel.page-demo.config.sections.titles.updates")}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@@ -13,10 +13,11 @@
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
loadES5("<%= entry %>");
|
||||
<% } %>
|
||||
}
|
||||
} else {
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
loadES5("<%= entry %>");
|
||||
<% } %>
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
30
package.json
30
package.json
@@ -25,15 +25,15 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.25.6",
|
||||
"@babel/runtime": "7.25.7",
|
||||
"@braintree/sanitize-url": "7.1.0",
|
||||
"@codemirror/autocomplete": "6.18.1",
|
||||
"@codemirror/commands": "6.6.2",
|
||||
"@codemirror/commands": "6.7.0",
|
||||
"@codemirror/language": "6.10.3",
|
||||
"@codemirror/legacy-modes": "6.4.1",
|
||||
"@codemirror/search": "6.5.6",
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@codemirror/view": "6.34.0",
|
||||
"@codemirror/view": "6.34.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.12.5",
|
||||
"@formatjs/intl-displaynames": "6.6.8",
|
||||
@@ -89,8 +89,8 @@
|
||||
"@polymer/polymer": "3.5.1",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.4.9",
|
||||
"@vaadin/vaadin-themable-mixin": "24.4.9",
|
||||
"@vaadin/combo-box": "24.4.11",
|
||||
"@vaadin/vaadin-themable-mixin": "24.4.11",
|
||||
"@vibrant/color": "3.2.1-alpha.1",
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
@@ -104,7 +104,7 @@
|
||||
"core-js": "3.38.1",
|
||||
"cropperjs": "1.6.2",
|
||||
"date-fns": "4.1.0",
|
||||
"date-fns-tz": "3.1.3",
|
||||
"date-fns-tz": "3.2.0",
|
||||
"deep-clone-simple": "1.1.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"dialog-polyfill": "0.5.6",
|
||||
@@ -151,12 +151,12 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.25.2",
|
||||
"@babel/core": "7.25.7",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.2",
|
||||
"@babel/plugin-proposal-decorators": "7.24.7",
|
||||
"@babel/plugin-transform-runtime": "7.25.4",
|
||||
"@babel/preset-env": "7.25.4",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@babel/plugin-proposal-decorators": "7.25.7",
|
||||
"@babel/plugin-transform-runtime": "7.25.7",
|
||||
"@babel/preset-env": "7.25.7",
|
||||
"@babel/preset-typescript": "7.25.7",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.15.1",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@lokalise/node-api": "12.7.0",
|
||||
@@ -172,7 +172,7 @@
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.17",
|
||||
"@types/chromecast-caf-sender": "1.0.10",
|
||||
"@types/color-name": "1.1.4",
|
||||
"@types/color-name": "2.0.0",
|
||||
"@types/glob": "8.1.0",
|
||||
"@types/html-minifier-terser": "7.0.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
@@ -195,17 +195,17 @@
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"chai": "5.1.1",
|
||||
"del": "7.1.0",
|
||||
"del": "8.0.0",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-airbnb-typescript": "18.0.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-import-resolver-webpack": "0.13.9",
|
||||
"eslint-plugin-import": "2.30.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-lit": "1.15.0",
|
||||
"eslint-plugin-lit-a11y": "4.1.4",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"eslint-plugin-wc": "2.1.1",
|
||||
"eslint-plugin-wc": "2.2.0",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.2.0",
|
||||
"glob": "11.0.0",
|
||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20240927.0"
|
||||
version = "20241010.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@@ -20,6 +20,15 @@ function findNestedItem(
|
||||
}, obj);
|
||||
}
|
||||
|
||||
function updateNestedItem(obj: any, path: ItemPath): any {
|
||||
const lastKey = path.pop()!;
|
||||
const parent = findNestedItem(obj, path);
|
||||
parent[lastKey] = Array.isArray(parent[lastKey])
|
||||
? [...parent[lastKey]]
|
||||
: [parent[lastKey]];
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function nestedArrayMove<A>(
|
||||
obj: A,
|
||||
oldIndex: number,
|
||||
@@ -27,14 +36,18 @@ export function nestedArrayMove<A>(
|
||||
oldPath?: ItemPath,
|
||||
newPath?: ItemPath
|
||||
): A {
|
||||
const newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
|
||||
let newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
|
||||
|
||||
if (oldPath) {
|
||||
newObj = updateNestedItem(newObj, [...oldPath]);
|
||||
}
|
||||
if (newPath) {
|
||||
newObj = updateNestedItem(newObj, [...newPath]);
|
||||
}
|
||||
|
||||
const from = oldPath ? findNestedItem(newObj, oldPath) : newObj;
|
||||
const to = newPath ? findNestedItem(newObj, newPath, true) : newObj;
|
||||
|
||||
if (!Array.isArray(from) || !Array.isArray(to)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const item = from.splice(oldIndex, 1)[0];
|
||||
to.splice(newIndex, 0, item);
|
||||
|
||||
|
@@ -204,6 +204,29 @@ export class HaDataTable extends LitElement {
|
||||
this._checkedRowsChanged();
|
||||
}
|
||||
|
||||
public select(ids: string[], clear?: boolean): void {
|
||||
if (clear) {
|
||||
this._checkedRows = [];
|
||||
}
|
||||
ids.forEach((id) => {
|
||||
const row = this._filteredData.find((data) => data[this.id] === id);
|
||||
if (row?.selectable !== false && !this._checkedRows.includes(id)) {
|
||||
this._checkedRows.push(id);
|
||||
}
|
||||
});
|
||||
this._checkedRowsChanged();
|
||||
}
|
||||
|
||||
public unselect(ids: string[]): void {
|
||||
ids.forEach((id) => {
|
||||
const index = this._checkedRows.indexOf(id);
|
||||
if (index > -1) {
|
||||
this._checkedRows.splice(index, 1);
|
||||
}
|
||||
});
|
||||
this._checkedRowsChanged();
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this._filteredData.length) {
|
||||
@@ -1011,6 +1034,7 @@ export class HaDataTable extends LitElement {
|
||||
/* @noflip */
|
||||
padding-inline-end: initial;
|
||||
width: 60px;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.mdc-data-table__table {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { mdiInvertColorsOff, mdiPalette } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@@ -8,8 +8,9 @@ import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { LocalizeKeys } from "../common/translations/localize";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import "./ha-md-divider";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
|
||||
@customElement("ha-color-picker")
|
||||
export class HaColorPicker extends LitElement {
|
||||
@@ -32,7 +33,17 @@ export class HaColorPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
_valueSelected(ev) {
|
||||
@query("ha-select") private _select?: HaSelect;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
// Refresh layout options when the field is connected to the DOM to ensure current value displayed
|
||||
this._select?.layoutOptions();
|
||||
}
|
||||
|
||||
private _valueSelected(ev) {
|
||||
ev.stopPropagation();
|
||||
if (!this.isConnected) return;
|
||||
const value = ev.target.value;
|
||||
this.value = value === this.defaultColor ? undefined : value;
|
||||
fireEvent(this, "value-changed", {
|
||||
@@ -41,7 +52,13 @@ export class HaColorPicker extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
const value = this.value || this.defaultColor;
|
||||
const value = this.value || this.defaultColor || "";
|
||||
|
||||
const isCustom = !(
|
||||
THEME_COLORS.has(value) ||
|
||||
value === "none" ||
|
||||
value === "state"
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
@@ -110,6 +127,14 @@ export class HaColorPicker extends LitElement {
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
${isCustom
|
||||
? html`
|
||||
<ha-list-item .value=${value} graphic="icon">
|
||||
${value}
|
||||
<span slot="graphic">${this.renderColorCircle(value)}</span>
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-console */
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -6,11 +7,14 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { handleWebRtcOffer, WebRtcAnswer } from "../data/camera";
|
||||
import { fetchWebRtcSettings } from "../data/rtsp_to_webrtc";
|
||||
import {
|
||||
fetchWebRtcClientConfiguration,
|
||||
handleWebRtcOffer,
|
||||
WebRtcAnswer,
|
||||
} from "../data/camera";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
|
||||
@@ -37,12 +41,11 @@ class HaWebRtcPlayer extends LitElement {
|
||||
@property({ type: Boolean, attribute: "playsinline" })
|
||||
public playsInline = false;
|
||||
|
||||
@property() public posterUrl!: string;
|
||||
@property({ attribute: "poster-url" }) public posterUrl?: string;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
// don't cache this, as we remove it on disconnects
|
||||
@query("#remote-stream") private _videoEl!: HTMLVideoElement;
|
||||
@query("#remote-stream", true) private _videoEl!: HTMLVideoElement;
|
||||
|
||||
private _peerConnection?: RTCPeerConnection;
|
||||
|
||||
@@ -59,7 +62,7 @@ class HaWebRtcPlayer extends LitElement {
|
||||
.muted=${this.muted}
|
||||
?playsinline=${this.playsInline}
|
||||
?controls=${this.controls}
|
||||
.poster=${this.posterUrl}
|
||||
poster=${ifDefined(this.posterUrl)}
|
||||
@loadeddata=${this._loadedData}
|
||||
></video>
|
||||
`;
|
||||
@@ -81,20 +84,30 @@ class HaWebRtcPlayer extends LitElement {
|
||||
if (!changedProperties.has("entityid")) {
|
||||
return;
|
||||
}
|
||||
if (!this._videoEl) {
|
||||
return;
|
||||
}
|
||||
this._startWebRtc();
|
||||
}
|
||||
|
||||
private async _startWebRtc(): Promise<void> {
|
||||
console.time("WebRTC");
|
||||
|
||||
this._error = undefined;
|
||||
|
||||
const configuration = await this._fetchPeerConfiguration();
|
||||
const peerConnection = new RTCPeerConnection(configuration);
|
||||
// Some cameras (such as nest) require a data channel to establish a stream
|
||||
// however, not used by any integrations.
|
||||
peerConnection.createDataChannel("dataSendChannel");
|
||||
console.timeLog("WebRTC", "start clientConfig");
|
||||
|
||||
const clientConfig = await fetchWebRtcClientConfiguration(
|
||||
this.hass,
|
||||
this.entityid
|
||||
);
|
||||
|
||||
console.timeLog("WebRTC", "end clientConfig", clientConfig);
|
||||
|
||||
const peerConnection = new RTCPeerConnection(clientConfig.configuration);
|
||||
|
||||
if (clientConfig.dataChannel) {
|
||||
// Some cameras (such as nest) require a data channel to establish a stream
|
||||
// however, not used by any integrations.
|
||||
peerConnection.createDataChannel(clientConfig.dataChannel);
|
||||
}
|
||||
peerConnection.addTransceiver("audio", { direction: "recvonly" });
|
||||
peerConnection.addTransceiver("video", { direction: "recvonly" });
|
||||
|
||||
@@ -102,30 +115,48 @@ class HaWebRtcPlayer extends LitElement {
|
||||
offerToReceiveAudio: true,
|
||||
offerToReceiveVideo: true,
|
||||
};
|
||||
|
||||
console.timeLog("WebRTC", "start createOffer", offerOptions);
|
||||
|
||||
const offer: RTCSessionDescriptionInit =
|
||||
await peerConnection.createOffer(offerOptions);
|
||||
|
||||
console.timeLog("WebRTC", "end createOffer", offer);
|
||||
|
||||
console.timeLog("WebRTC", "start setLocalDescription");
|
||||
|
||||
await peerConnection.setLocalDescription(offer);
|
||||
|
||||
console.timeLog("WebRTC", "end setLocalDescription");
|
||||
|
||||
console.timeLog("WebRTC", "start iceResolver");
|
||||
|
||||
let candidates = ""; // Build an Offer SDP string with ice candidates
|
||||
const iceResolver = new Promise<void>((resolve) => {
|
||||
peerConnection.addEventListener("icecandidate", async (event) => {
|
||||
peerConnection.addEventListener("icecandidate", (event) => {
|
||||
if (!event.candidate?.candidate) {
|
||||
resolve(); // Gathering complete
|
||||
return;
|
||||
}
|
||||
console.timeLog("WebRTC", "iceResolver candidate", event.candidate);
|
||||
candidates += `a=${event.candidate.candidate}\r\n`;
|
||||
});
|
||||
});
|
||||
await iceResolver;
|
||||
|
||||
console.timeLog("WebRTC", "end iceResolver", candidates);
|
||||
|
||||
const offer_sdp = offer.sdp! + candidates;
|
||||
|
||||
let webRtcAnswer: WebRtcAnswer;
|
||||
try {
|
||||
console.timeLog("WebRTC", "start WebRTCOffer", offer_sdp);
|
||||
webRtcAnswer = await handleWebRtcOffer(
|
||||
this.hass,
|
||||
this.entityid,
|
||||
offer_sdp
|
||||
);
|
||||
console.timeLog("WebRTC", "end webRtcOffer", webRtcAnswer);
|
||||
} catch (err: any) {
|
||||
this._error = "Failed to start WebRTC stream: " + err.message;
|
||||
peerConnection.close();
|
||||
@@ -135,6 +166,7 @@ class HaWebRtcPlayer extends LitElement {
|
||||
// Setup callbacks to render remote stream once media tracks are discovered.
|
||||
const remoteStream = new MediaStream();
|
||||
peerConnection.addEventListener("track", (event) => {
|
||||
console.timeLog("WebRTC", "track", event);
|
||||
remoteStream.addTrack(event.track);
|
||||
this._videoEl.srcObject = remoteStream;
|
||||
});
|
||||
@@ -146,7 +178,9 @@ class HaWebRtcPlayer extends LitElement {
|
||||
sdp: webRtcAnswer.answer,
|
||||
});
|
||||
try {
|
||||
console.timeLog("WebRTC", "start setRemoteDescription", remoteDesc);
|
||||
await peerConnection.setRemoteDescription(remoteDesc);
|
||||
console.timeLog("WebRTC", "end setRemoteDescription");
|
||||
} catch (err: any) {
|
||||
this._error = "Failed to connect WebRTC stream: " + err.message;
|
||||
peerConnection.close();
|
||||
@@ -155,23 +189,6 @@ class HaWebRtcPlayer extends LitElement {
|
||||
this._peerConnection = peerConnection;
|
||||
}
|
||||
|
||||
private async _fetchPeerConfiguration(): Promise<RTCConfiguration> {
|
||||
if (!isComponentLoaded(this.hass!, "rtsp_to_webrtc")) {
|
||||
return {};
|
||||
}
|
||||
const settings = await fetchWebRtcSettings(this.hass!);
|
||||
if (!settings || !settings.stun_server) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
iceServers: [
|
||||
{
|
||||
urls: [`stun:${settings.stun_server!}`],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private _cleanUp() {
|
||||
if (this._remoteStream) {
|
||||
this._remoteStream.getTracks().forEach((track) => {
|
||||
@@ -190,6 +207,8 @@ class HaWebRtcPlayer extends LitElement {
|
||||
}
|
||||
|
||||
private _loadedData() {
|
||||
console.timeLog("WebRTC", "loadedData");
|
||||
console.timeEnd("WebRTC");
|
||||
// @ts-ignore
|
||||
fireEvent(this, "load");
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import { Context, HomeAssistant } from "../types";
|
||||
import { BlueprintInput } from "./blueprint";
|
||||
import { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
import { Action, MODES, migrateAutomationAction } from "./script";
|
||||
import { createSearchParam } from "../common/url/search-params";
|
||||
|
||||
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
|
||||
export const AUTOMATION_DEFAULT_MAX = 10;
|
||||
@@ -462,9 +463,13 @@ export const flattenTriggers = (
|
||||
return flatTriggers;
|
||||
};
|
||||
|
||||
export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
|
||||
export const showAutomationEditor = (
|
||||
data?: Partial<AutomationConfig>,
|
||||
expanded?: boolean
|
||||
) => {
|
||||
initialAutomationEditorData = data;
|
||||
navigate("/config/automation/edit/new");
|
||||
const params = expanded ? `?${createSearchParam({ expanded: "1" })}` : "";
|
||||
navigate(`/config/automation/edit/new${params}`);
|
||||
};
|
||||
|
||||
export const duplicateAutomation = (config: AutomationConfig) => {
|
||||
|
@@ -133,3 +133,17 @@ export const isCameraMediaSource = (mediaContentId: string) =>
|
||||
|
||||
export const getEntityIdFromCameraMediaSource = (mediaContentId: string) =>
|
||||
mediaContentId.substring(CAMERA_MEDIA_SOURCE_PREFIX.length);
|
||||
|
||||
export interface WebRTCClientConfiguration {
|
||||
configuration: RTCConfiguration;
|
||||
dataChannel?: string;
|
||||
}
|
||||
|
||||
export const fetchWebRtcClientConfiguration = async (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
) =>
|
||||
hass.callWS<WebRTCClientConfiguration>({
|
||||
type: "camera/webrtc/get_client_config",
|
||||
entity_id: entityId,
|
||||
});
|
||||
|
@@ -28,6 +28,7 @@ export interface UrlActionConfig extends BaseActionConfig {
|
||||
|
||||
export interface MoreInfoActionConfig extends BaseActionConfig {
|
||||
action: "more-info";
|
||||
entity_id?: string;
|
||||
}
|
||||
|
||||
export interface AssistActionConfig extends BaseActionConfig {
|
||||
|
@@ -332,3 +332,6 @@ export const getDisplayUnit = (
|
||||
|
||||
export const isExternalStatistic = (statisticsId: string): boolean =>
|
||||
statisticsId.includes(":");
|
||||
|
||||
export const updateStatisticsIssues = (hass: HomeAssistant) =>
|
||||
hass.callWS({ type: "recorder/update_statistics_issues" });
|
||||
|
@@ -1,10 +0,0 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface WebRtcSettings {
|
||||
stun_server?: string;
|
||||
}
|
||||
|
||||
export const fetchWebRtcSettings = async (hass: HomeAssistant) =>
|
||||
hass.callWS<WebRtcSettings>({
|
||||
type: "rtsp_to_webrtc/get_settings",
|
||||
});
|
@@ -28,6 +28,7 @@ import {
|
||||
} from "./automation";
|
||||
import { BlueprintInput } from "./blueprint";
|
||||
import { computeObjectId } from "../common/entity/compute_object_id";
|
||||
import { createSearchParam } from "../common/url/search-params";
|
||||
|
||||
export const MODES = ["single", "restart", "queued", "parallel"] as const;
|
||||
export const MODES_MAX = ["queued", "parallel"] as const;
|
||||
@@ -347,9 +348,13 @@ export const getScriptStateConfig = (hass: HomeAssistant, entity_id: string) =>
|
||||
entity_id,
|
||||
});
|
||||
|
||||
export const showScriptEditor = (data?: Partial<ScriptConfig>) => {
|
||||
export const showScriptEditor = (
|
||||
data?: Partial<ScriptConfig>,
|
||||
expanded?: boolean
|
||||
) => {
|
||||
inititialScriptEditorData = data;
|
||||
navigate("/config/script/edit/new");
|
||||
const params = expanded ? `?${createSearchParam({ expanded: "1" })}` : "";
|
||||
navigate(`/config/script/edit/new${params}`);
|
||||
};
|
||||
|
||||
export const getScriptEditorInitData = () => {
|
||||
|
@@ -18,7 +18,7 @@ export interface ThreadDataSet {
|
||||
channel: number | null;
|
||||
created: string;
|
||||
dataset_id: string;
|
||||
extended_pan_id: string | null;
|
||||
extended_pan_id: string;
|
||||
network_name: string;
|
||||
pan_id: string | null;
|
||||
preferred_border_agent_id: string | null;
|
||||
|
@@ -72,8 +72,8 @@ export const timerTimeRemaining = (
|
||||
|
||||
if (stateObj.state === "active") {
|
||||
const now = new Date().getTime();
|
||||
const madeActive = new Date(stateObj.last_changed).getTime();
|
||||
timeRemaining = Math.max(timeRemaining - (now - madeActive) / 1000, 0);
|
||||
const finishes = new Date(stateObj.attributes.finishes_at).getTime();
|
||||
timeRemaining = Math.max((finishes - now) / 1000, 0);
|
||||
}
|
||||
|
||||
return timeRemaining;
|
||||
|
@@ -9,23 +9,15 @@ import {
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { HASSDomEvent, fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-icon-button";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../../data/area_registry";
|
||||
import {
|
||||
DataEntryFlowStep,
|
||||
subscribeDataEntryFlowProgressed,
|
||||
} from "../../data/data_entry_flow";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../data/device_registry";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
@@ -62,7 +54,7 @@ declare global {
|
||||
|
||||
@customElement("dialog-data-entry-flow")
|
||||
class DataEntryFlowDialog extends LitElement {
|
||||
public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: DataEntryFlowDialogParams;
|
||||
|
||||
@@ -76,16 +68,8 @@ class DataEntryFlowDialog extends LitElement {
|
||||
// Null means we need to pick a config flow
|
||||
| null;
|
||||
|
||||
@state() private _devices?: DeviceRegistryEntry[];
|
||||
|
||||
@state() private _areas?: AreaRegistryEntry[];
|
||||
|
||||
@state() private _handler?: string;
|
||||
|
||||
private _unsubAreas?: UnsubscribeFunc;
|
||||
|
||||
private _unsubDevices?: UnsubscribeFunc;
|
||||
|
||||
private _unsubDataEntryFlowProgressed?: Promise<UnsubscribeFunc>;
|
||||
|
||||
public async showDialog(params: DataEntryFlowDialogParams): Promise<void> {
|
||||
@@ -183,16 +167,7 @@ class DataEntryFlowDialog extends LitElement {
|
||||
this._loading = undefined;
|
||||
this._step = undefined;
|
||||
this._params = undefined;
|
||||
this._devices = undefined;
|
||||
this._handler = undefined;
|
||||
if (this._unsubAreas) {
|
||||
this._unsubAreas();
|
||||
this._unsubAreas = undefined;
|
||||
}
|
||||
if (this._unsubDevices) {
|
||||
this._unsubDevices();
|
||||
this._unsubDevices = undefined;
|
||||
}
|
||||
if (this._unsubDataEntryFlowProgressed) {
|
||||
this._unsubDataEntryFlowProgressed.then((unsub) => {
|
||||
unsub();
|
||||
@@ -309,25 +284,13 @@ class DataEntryFlowDialog extends LitElement {
|
||||
.hass=${this.hass}
|
||||
></step-flow-menu>
|
||||
`
|
||||
: this._devices === undefined ||
|
||||
this._areas === undefined
|
||||
? // When it's a create entry result, we will fetch device & area registry
|
||||
html`
|
||||
<step-flow-loading
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.hass=${this.hass}
|
||||
loadingReason="loading_devices_areas"
|
||||
></step-flow-loading>
|
||||
`
|
||||
: html`
|
||||
<step-flow-create-entry
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
.devices=${this._devices}
|
||||
.areas=${this._areas}
|
||||
></step-flow-create-entry>
|
||||
`}
|
||||
: html`
|
||||
<step-flow-create-entry
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
></step-flow-create-entry>
|
||||
`}
|
||||
`}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
@@ -351,32 +314,6 @@ class DataEntryFlowDialog extends LitElement {
|
||||
// external and progress step will send update event from the backend, so we should subscribe to them
|
||||
this._subscribeDataEntryFlowProgressed();
|
||||
}
|
||||
if (this._step.type === "create_entry") {
|
||||
if (this._step.result && this._params!.flowConfig.loadDevicesAndAreas) {
|
||||
this._fetchDevices(this._step.result.entry_id);
|
||||
this._fetchAreas();
|
||||
} else {
|
||||
this._devices = [];
|
||||
this._areas = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchDevices(configEntryId) {
|
||||
this._unsubDevices = subscribeDeviceRegistry(
|
||||
this.hass.connection,
|
||||
(devices) => {
|
||||
this._devices = devices.filter((device) =>
|
||||
device.config_entries.includes(configEntryId)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async _fetchAreas() {
|
||||
this._unsubAreas = subscribeAreaRegistry(this.hass.connection, (areas) => {
|
||||
this._areas = areas;
|
||||
});
|
||||
}
|
||||
|
||||
private async _processStep(
|
||||
|
@@ -20,7 +20,7 @@ export const showConfigFlowDialog = (
|
||||
): void =>
|
||||
showFlowDialog(element, dialogParams, {
|
||||
flowType: "config_flow",
|
||||
loadDevicesAndAreas: true,
|
||||
showDevices: true,
|
||||
createFlow: async (hass, handler) => {
|
||||
const [step] = await Promise.all([
|
||||
createConfigFlow(hass, handler, dialogParams.entryId),
|
||||
|
@@ -17,7 +17,7 @@ import type { HomeAssistant } from "../../types";
|
||||
export interface FlowConfig {
|
||||
flowType: FlowType;
|
||||
|
||||
loadDevicesAndAreas: boolean;
|
||||
showDevices: boolean;
|
||||
|
||||
createFlow(hass: HomeAssistant, handler: string): Promise<DataEntryFlowStep>;
|
||||
|
||||
@@ -134,8 +134,7 @@ export interface FlowConfig {
|
||||
export type LoadingReason =
|
||||
| "loading_handlers"
|
||||
| "loading_flow"
|
||||
| "loading_step"
|
||||
| "loading_devices_areas";
|
||||
| "loading_step";
|
||||
|
||||
export interface DataEntryFlowDialogParams {
|
||||
startFlowHandler?: string;
|
||||
|
@@ -29,7 +29,7 @@ export const showOptionsFlowDialog = (
|
||||
},
|
||||
{
|
||||
flowType: "options_flow",
|
||||
loadDevicesAndAreas: false,
|
||||
showDevices: false,
|
||||
createFlow: async (hass, handler) => {
|
||||
const [step] = await Promise.all([
|
||||
createOptionsFlow(hass, handler),
|
||||
|
@@ -4,6 +4,7 @@ import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
@@ -34,7 +35,16 @@ class StepFlowCreateEntry extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry;
|
||||
|
||||
@property({ attribute: false }) public devices!: DeviceRegistryEntry[];
|
||||
private _devices = memoizeOne(
|
||||
(
|
||||
showDevices: boolean,
|
||||
devices: DeviceRegistryEntry[],
|
||||
entry_id?: string
|
||||
) =>
|
||||
showDevices && entry_id
|
||||
? devices.filter((device) => device.config_entries.includes(entry_id))
|
||||
: []
|
||||
);
|
||||
|
||||
private _deviceEntities = memoizeOne(
|
||||
(
|
||||
@@ -50,35 +60,48 @@ class StepFlowCreateEntry extends LitElement {
|
||||
);
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
if (!changedProps.has("devices") && !changedProps.has("hass")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const devices = this._devices(
|
||||
this.flowConfig.showDevices,
|
||||
Object.values(this.hass.devices),
|
||||
this.step.result?.entry_id
|
||||
);
|
||||
|
||||
if (
|
||||
(changedProps.has("devices") || changedProps.has("hass")) &&
|
||||
this.devices.length === 1
|
||||
devices.length !== 1 ||
|
||||
devices[0].primary_config_entry !== this.step.result?.entry_id
|
||||
) {
|
||||
// integration_type === "device"
|
||||
const assistSatellites = this._deviceEntities(
|
||||
this.devices[0].id,
|
||||
Object.values(this.hass.entities),
|
||||
"assist_satellite"
|
||||
);
|
||||
if (
|
||||
assistSatellites.length &&
|
||||
assistSatellites.some((satellite) =>
|
||||
assistSatelliteSupportsSetupFlow(
|
||||
this.hass.states[satellite.entity_id]
|
||||
)
|
||||
)
|
||||
) {
|
||||
this._flowDone();
|
||||
showVoiceAssistantSetupDialog(this, {
|
||||
deviceId: this.devices[0].id,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const assistSatellites = this._deviceEntities(
|
||||
devices[0].id,
|
||||
Object.values(this.hass.entities),
|
||||
"assist_satellite"
|
||||
);
|
||||
if (
|
||||
assistSatellites.length &&
|
||||
assistSatellites.some((satellite) =>
|
||||
assistSatelliteSupportsSetupFlow(this.hass.states[satellite.entity_id])
|
||||
)
|
||||
) {
|
||||
this._flowDone();
|
||||
showVoiceAssistantSetupDialog(this, {
|
||||
deviceId: devices[0].id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const localize = this.hass.localize;
|
||||
|
||||
const devices = this._devices(
|
||||
this.flowConfig.showDevices,
|
||||
Object.values(this.hass.devices),
|
||||
this.step.result?.entry_id
|
||||
);
|
||||
return html`
|
||||
<h2>${localize("ui.panel.config.integrations.config_flow.success")}!</h2>
|
||||
<div class="content">
|
||||
@@ -89,9 +112,9 @@ class StepFlowCreateEntry extends LitElement {
|
||||
"ui.panel.config.integrations.config_flow.not_loaded"
|
||||
)}</span
|
||||
>`
|
||||
: ""}
|
||||
${this.devices.length === 0
|
||||
? ""
|
||||
: nothing}
|
||||
${devices.length === 0
|
||||
? nothing
|
||||
: html`
|
||||
<p>
|
||||
${localize(
|
||||
@@ -99,7 +122,7 @@ class StepFlowCreateEntry extends LitElement {
|
||||
)}:
|
||||
</p>
|
||||
<div class="devices">
|
||||
${this.devices.map(
|
||||
${devices.map(
|
||||
(device) => html`
|
||||
<div class="device">
|
||||
<div>
|
||||
|
@@ -18,6 +18,7 @@ import {
|
||||
updateReleaseNotes,
|
||||
} from "../../../data/update";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showAlertDialog } from "../../generic/show-dialog-box";
|
||||
|
||||
@customElement("more-info-update")
|
||||
class MoreInfoUpdate extends LitElement {
|
||||
@@ -127,29 +128,27 @@ class MoreInfoUpdate extends LitElement {
|
||||
</ha-formfield> `
|
||||
: ""}
|
||||
<div class="actions">
|
||||
${this.stateObj.attributes.auto_update
|
||||
? ""
|
||||
: this.stateObj.state === BINARY_STATE_OFF &&
|
||||
this.stateObj.attributes.skipped_version
|
||||
? html`
|
||||
<mwc-button @click=${this._handleClearSkipped}>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.clear_skipped"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: html`
|
||||
<mwc-button
|
||||
@click=${this._handleSkip}
|
||||
.disabled=${skippedVersion ||
|
||||
this.stateObj.state === BINARY_STATE_OFF ||
|
||||
updateIsInstalling(this.stateObj)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.skip"
|
||||
)}
|
||||
</mwc-button>
|
||||
`}
|
||||
${this.stateObj.state === BINARY_STATE_OFF &&
|
||||
this.stateObj.attributes.skipped_version
|
||||
? html`
|
||||
<mwc-button @click=${this._handleClearSkipped}>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.clear_skipped"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: html`
|
||||
<mwc-button
|
||||
@click=${this._handleSkip}
|
||||
.disabled=${skippedVersion ||
|
||||
this.stateObj.state === BINARY_STATE_OFF ||
|
||||
updateIsInstalling(this.stateObj)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.skip"
|
||||
)}
|
||||
</mwc-button>
|
||||
`}
|
||||
${supportsFeature(this.stateObj, UpdateEntityFeature.INSTALL)
|
||||
? html`
|
||||
<mwc-button
|
||||
@@ -211,6 +210,17 @@ class MoreInfoUpdate extends LitElement {
|
||||
}
|
||||
|
||||
private _handleSkip(): void {
|
||||
if (this.stateObj!.attributes.auto_update) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.auto_update_enabled_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.auto_update_enabled_text"
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.hass.callService("update", "skip", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
});
|
||||
|
@@ -2,7 +2,7 @@ import { css, html, LitElement, nothing, PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-circular-progress";
|
||||
import { UNAVAILABLE } from "../../data/entity";
|
||||
import { OFF, ON, UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
|
||||
@@ -14,6 +14,8 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
||||
|
||||
private _updated = false;
|
||||
|
||||
private _refreshTimeout?: number;
|
||||
|
||||
protected override willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
@@ -28,17 +30,19 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
||||
const oldState = oldHass.states[this.updateEntityId];
|
||||
const newState = this.hass.states[this.updateEntityId];
|
||||
if (
|
||||
oldState?.state === UNAVAILABLE &&
|
||||
newState?.state !== UNAVAILABLE
|
||||
(oldState?.state === UNAVAILABLE &&
|
||||
newState?.state !== UNAVAILABLE) ||
|
||||
(oldState?.state !== ON && newState?.state === ON)
|
||||
) {
|
||||
// Device is rebooted, let's move on
|
||||
this._tryUpdate();
|
||||
this._tryUpdate(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.has("updateEntityId")) {
|
||||
this._tryUpdate();
|
||||
this._tryUpdate(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +58,11 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
||||
|
||||
return html`<div class="content">
|
||||
<img src="/static/icons/casita/loading.png" />
|
||||
<h1>Updating your voice assistant</h1>
|
||||
<h1>
|
||||
${stateObj.state === OFF || stateObj.state === UNKNOWN
|
||||
? "Checking for updates"
|
||||
: "Updating your voice assistant"}
|
||||
</h1>
|
||||
<p class="secondary">
|
||||
We are making sure you have the latest and greatest version of your
|
||||
voice assistant. This may take a few minutes.
|
||||
@@ -75,15 +83,13 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private async _tryUpdate() {
|
||||
private async _tryUpdate(refreshUpdate: boolean) {
|
||||
clearTimeout(this._refreshTimeout);
|
||||
if (!this.updateEntityId) {
|
||||
return;
|
||||
}
|
||||
const updateEntity = this.hass.states[this.updateEntityId];
|
||||
if (
|
||||
updateEntity &&
|
||||
this.hass.states[updateEntity.entity_id].state === "on"
|
||||
) {
|
||||
if (updateEntity && this.hass.states[updateEntity.entity_id].state === ON) {
|
||||
this._updated = true;
|
||||
await this.hass.callService(
|
||||
"update",
|
||||
@@ -91,6 +97,16 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
||||
{},
|
||||
{ entity_id: updateEntity.entity_id }
|
||||
);
|
||||
} else if (refreshUpdate) {
|
||||
await this.hass.callService(
|
||||
"homeassistant",
|
||||
"update_entity",
|
||||
{},
|
||||
{ entity_id: this.updateEntityId }
|
||||
);
|
||||
this._refreshTimeout = window.setTimeout(() => {
|
||||
this._nextStep();
|
||||
}, 5000);
|
||||
} else {
|
||||
this._nextStep();
|
||||
}
|
||||
|
@@ -141,9 +141,10 @@ interface EMOutgoingMessageImprovScan extends EMMessage {
|
||||
interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage {
|
||||
type: "thread/store_in_platform_keychain";
|
||||
payload: {
|
||||
mac_extended_address: string;
|
||||
border_agent_id: string;
|
||||
mac_extended_address: string | null;
|
||||
border_agent_id: string | null;
|
||||
active_operational_dataset: string;
|
||||
extended_pan_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -156,6 +156,15 @@ export default class HaAutomationAction extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public expandAll() {
|
||||
const rows = this.shadowRoot!.querySelectorAll<HaAutomationActionRow>(
|
||||
"ha-automation-action-row"
|
||||
)!;
|
||||
rows.forEach((row) => {
|
||||
row.expand();
|
||||
});
|
||||
}
|
||||
|
||||
private _addActionDialog() {
|
||||
showAddAutomationElementDialog(this, {
|
||||
type: "action",
|
||||
|
@@ -55,12 +55,12 @@ export class HaSceneAction extends LitElement implements ActionElement {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.action,
|
||||
service: "scene.turn_on",
|
||||
action: "scene.turn_on",
|
||||
target: {
|
||||
entity_id: ev.detail.value,
|
||||
},
|
||||
metadata: {},
|
||||
},
|
||||
} as SceneAction,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -52,7 +52,7 @@ export class HaPlayMediaAction extends LitElement implements ActionElement {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.action,
|
||||
service: "media_player.play_media",
|
||||
action: "media_player.play_media",
|
||||
target: { entity_id: ev.detail.value.entity_id },
|
||||
data: {
|
||||
media_content_id: ev.detail.value.media_content_id,
|
||||
|
@@ -117,6 +117,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
||||
.value=${this._action}
|
||||
.disabled=${this.disabled}
|
||||
.showAdvanced=${this.hass.userData?.showAdvanced}
|
||||
.hidePicker=${!!this._action.metadata}
|
||||
@value-changed=${this._actionChanged}
|
||||
></ha-service-control>
|
||||
${domain && service && this.hass.services[domain]?.[service]?.response
|
||||
|
@@ -106,6 +106,15 @@ export default class HaAutomationCondition extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public expandAll() {
|
||||
const rows = this.shadowRoot!.querySelectorAll<HaAutomationConditionRow>(
|
||||
"ha-automation-condition-row"
|
||||
)!;
|
||||
rows.forEach((row) => {
|
||||
row.expand();
|
||||
});
|
||||
}
|
||||
|
||||
private get nested() {
|
||||
return this.path !== undefined;
|
||||
}
|
||||
|
@@ -1,7 +1,14 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ensureArray } from "../../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
@@ -21,6 +28,14 @@ import { documentationUrl } from "../../../util/documentation-url";
|
||||
import "./action/ha-automation-action";
|
||||
import "./condition/ha-automation-condition";
|
||||
import "./trigger/ha-automation-trigger";
|
||||
import type HaAutomationTrigger from "./trigger/ha-automation-trigger";
|
||||
import type HaAutomationAction from "./action/ha-automation-action";
|
||||
import type HaAutomationCondition from "./condition/ha-automation-condition";
|
||||
import {
|
||||
extractSearchParam,
|
||||
removeSearchParam,
|
||||
} from "../../../common/url/search-params";
|
||||
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
||||
|
||||
@customElement("manual-automation-editor")
|
||||
export class HaManualAutomationEditor extends LitElement {
|
||||
@@ -36,6 +51,31 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
const expanded = extractSearchParam("expanded");
|
||||
if (expanded === "1") {
|
||||
this._clearParam("expanded");
|
||||
const items = this.shadowRoot!.querySelectorAll<
|
||||
HaAutomationTrigger | HaAutomationCondition | HaAutomationAction
|
||||
>("ha-automation-trigger, ha-automation-condition, ha-automation-action");
|
||||
|
||||
items.forEach((el) => {
|
||||
el.updateComplete.then(() => {
|
||||
el.expandAll();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _clearParam(param: string) {
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
constructUrlCurrentPath(removeSearchParam(param))
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.stateObj?.state === "off"
|
||||
|
@@ -179,6 +179,15 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public expandAll() {
|
||||
const rows = this.shadowRoot!.querySelectorAll<HaAutomationTriggerRow>(
|
||||
"ha-automation-trigger-row"
|
||||
)!;
|
||||
rows.forEach((row) => {
|
||||
row.expand();
|
||||
});
|
||||
}
|
||||
|
||||
private _getKey(action: Trigger) {
|
||||
if (!this._triggerKeys.has(action)) {
|
||||
this._triggerKeys.set(action, Math.random().toString());
|
||||
|
@@ -1,23 +0,0 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import {
|
||||
DeviceAction,
|
||||
localizeDeviceAutomationAction,
|
||||
} from "../../../../data/device_automation";
|
||||
import { HaDeviceAutomationCard } from "./ha-device-automation-card";
|
||||
|
||||
@customElement("ha-device-actions-card")
|
||||
export class HaDeviceActionsCard extends HaDeviceAutomationCard<DeviceAction> {
|
||||
readonly type = "action";
|
||||
|
||||
readonly headerKey = "ui.panel.config.devices.automation.actions.caption";
|
||||
|
||||
constructor() {
|
||||
super(localizeDeviceAutomationAction);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-device-actions-card": HaDeviceActionsCard;
|
||||
}
|
||||
}
|
@@ -1,142 +0,0 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/chips/ha-assist-chip";
|
||||
import "../../../../components/chips/ha-chip-set";
|
||||
import { showAutomationEditor } from "../../../../data/automation";
|
||||
import {
|
||||
DeviceAction,
|
||||
DeviceAutomation,
|
||||
} from "../../../../data/device_automation";
|
||||
import { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||
import { showScriptEditor } from "../../../../data/script";
|
||||
import { buttonLinkStyle } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"entry-selected": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class HaDeviceAutomationCard<
|
||||
T extends DeviceAutomation,
|
||||
> extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public deviceId?: string;
|
||||
|
||||
@property({ type: Boolean }) public script = false;
|
||||
|
||||
@property({ attribute: false }) public automations: T[] = [];
|
||||
|
||||
@property({ attribute: false }) entityReg?: EntityRegistryEntry[];
|
||||
|
||||
@state() public _showSecondary = false;
|
||||
|
||||
abstract headerKey: Parameters<typeof this.hass.localize>[0];
|
||||
|
||||
abstract type: "action" | "condition" | "trigger";
|
||||
|
||||
private _localizeDeviceAutomation: (
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
automation: T
|
||||
) => string;
|
||||
|
||||
constructor(
|
||||
localizeDeviceAutomation: HaDeviceAutomationCard<T>["_localizeDeviceAutomation"]
|
||||
) {
|
||||
super();
|
||||
this._localizeDeviceAutomation = localizeDeviceAutomation;
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps): boolean {
|
||||
if (changedProps.has("deviceId") || changedProps.has("automations")) {
|
||||
return true;
|
||||
}
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.language !== this.hass.language) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this.automations.length === 0 || !this.entityReg) {
|
||||
return nothing;
|
||||
}
|
||||
const automations = this._showSecondary
|
||||
? this.automations
|
||||
: this.automations.filter(
|
||||
(automation) => automation.metadata?.secondary === false
|
||||
);
|
||||
return html`
|
||||
<h3>${this.hass.localize(this.headerKey)}</h3>
|
||||
<div class="content">
|
||||
<ha-chip-set>
|
||||
${automations.map(
|
||||
(automation, idx) => html`
|
||||
<ha-assist-chip
|
||||
filled
|
||||
.index=${idx}
|
||||
@click=${this._handleAutomationClicked}
|
||||
class=${automation.metadata?.secondary ? "secondary" : ""}
|
||||
.label=${this._localizeDeviceAutomation(
|
||||
this.hass,
|
||||
this.entityReg!,
|
||||
automation
|
||||
)}
|
||||
>
|
||||
</ha-assist-chip>
|
||||
`
|
||||
)}
|
||||
</ha-chip-set>
|
||||
${!this._showSecondary && automations.length < this.automations.length
|
||||
? html`<button class="link" @click=${this._toggleSecondary}>
|
||||
Show ${this.automations.length - automations.length} more...
|
||||
</button>`
|
||||
: ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _toggleSecondary() {
|
||||
this._showSecondary = !this._showSecondary;
|
||||
}
|
||||
|
||||
private _handleAutomationClicked(ev: CustomEvent) {
|
||||
const automation = { ...this.automations[(ev.currentTarget as any).index] };
|
||||
if (!automation) {
|
||||
return;
|
||||
}
|
||||
delete automation.metadata;
|
||||
if (this.script) {
|
||||
showScriptEditor({ sequence: [automation as DeviceAction] });
|
||||
fireEvent(this, "entry-selected");
|
||||
return;
|
||||
}
|
||||
const data = {};
|
||||
data[this.type] = [automation];
|
||||
showAutomationEditor(data);
|
||||
fireEvent(this, "entry-selected");
|
||||
}
|
||||
|
||||
static styles = [
|
||||
buttonLinkStyle,
|
||||
css`
|
||||
h3 {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.secondary {
|
||||
--ha-assist-chip-filled-container-color: rgba(
|
||||
var(--rgb-primary-text-color),
|
||||
0.07
|
||||
);
|
||||
}
|
||||
button.link {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
@@ -1,8 +1,18 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
mdiAbTesting,
|
||||
mdiGestureTap,
|
||||
mdiPencilOutline,
|
||||
mdiRoomService,
|
||||
} from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-dialog";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../../../common/mwc/handle-request-selected-event";
|
||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||
import {
|
||||
AutomationConfig,
|
||||
showAutomationEditor,
|
||||
} from "../../../../data/automation";
|
||||
import {
|
||||
DeviceAction,
|
||||
DeviceCondition,
|
||||
@@ -12,11 +22,9 @@ import {
|
||||
fetchDeviceTriggers,
|
||||
sortDeviceAutomations,
|
||||
} from "../../../../data/device_automation";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import { ScriptConfig, showScriptEditor } from "../../../../data/script";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import "./ha-device-actions-card";
|
||||
import "./ha-device-conditions-card";
|
||||
import "./ha-device-triggers-card";
|
||||
import { DeviceAutomationDialogParams } from "./show-dialog-device-automation";
|
||||
|
||||
@customElement("dialog-device-automation")
|
||||
@@ -77,75 +85,184 @@ export class DialogDeviceAutomation extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleRowClick = (ev) => {
|
||||
if (!shouldHandleRequestSelectedEvent(ev) || !this._params) {
|
||||
return;
|
||||
}
|
||||
const type = (ev.currentTarget as any).type;
|
||||
const isScript = this._params.script;
|
||||
|
||||
this.closeDialog();
|
||||
|
||||
if (isScript) {
|
||||
const newScript = {} as ScriptConfig;
|
||||
if (type === "action") {
|
||||
newScript.sequence = [this._actions[0]];
|
||||
}
|
||||
showScriptEditor(newScript, true);
|
||||
} else {
|
||||
const newAutomation = {} as AutomationConfig;
|
||||
if (type === "trigger") {
|
||||
newAutomation.triggers = [this._triggers[0]];
|
||||
}
|
||||
if (type === "condition") {
|
||||
newAutomation.conditions = [this._conditions[0]];
|
||||
}
|
||||
if (type === "action") {
|
||||
newAutomation.actions = [this._actions[0]];
|
||||
}
|
||||
showAutomationEditor(newAutomation, true);
|
||||
}
|
||||
};
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const mode = this._params.script ? "script" : "automation";
|
||||
|
||||
const title = this.hass.localize(`ui.panel.config.devices.${mode}.create`, {
|
||||
type: this.hass.localize(
|
||||
`ui.panel.config.devices.type.${
|
||||
this._params.device.entry_type || "device"
|
||||
}`
|
||||
),
|
||||
});
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
hideActions
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${this.hass.localize(
|
||||
`ui.panel.config.devices.${
|
||||
this._params.script ? "script" : "automation"
|
||||
}.create`,
|
||||
{
|
||||
type: this.hass.localize(
|
||||
`ui.panel.config.devices.type.${
|
||||
this._params.device.entry_type || "device"
|
||||
}`
|
||||
),
|
||||
}
|
||||
)}
|
||||
.heading=${createCloseHeading(this.hass, title)}
|
||||
>
|
||||
<div @entry-selected=${this.closeDialog}>
|
||||
<mwc-list
|
||||
innerRole="listbox"
|
||||
itemRoles="option"
|
||||
innerAriaLabel="Create new automation"
|
||||
rootTabbable
|
||||
dialogInitialFocus
|
||||
>
|
||||
${this._triggers.length
|
||||
? html`
|
||||
<ha-list-item
|
||||
hasmeta
|
||||
twoline
|
||||
graphic="icon"
|
||||
.type=${"trigger"}
|
||||
@request-selected=${this._handleRowClick}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiGestureTap}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.devices.automation.triggers.title`
|
||||
)}
|
||||
<span slot="secondary">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.devices.automation.triggers.description`
|
||||
)}
|
||||
</span>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
${this._conditions.length
|
||||
? html`
|
||||
<ha-list-item
|
||||
hasmeta
|
||||
twoline
|
||||
graphic="icon"
|
||||
.type=${"condition"}
|
||||
@request-selected=${this._handleRowClick}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiAbTesting}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.devices.automation.conditions.title`
|
||||
)}
|
||||
<span slot="secondary">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.devices.automation.conditions.description`
|
||||
)}
|
||||
</span>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
${this._actions.length
|
||||
? html`
|
||||
<ha-list-item
|
||||
hasmeta
|
||||
twoline
|
||||
graphic="icon"
|
||||
.type=${"action"}
|
||||
@request-selected=${this._handleRowClick}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiRoomService}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.devices.${mode}.actions.title`
|
||||
)}
|
||||
<span slot="secondary">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.devices.${mode}.actions.description`
|
||||
)}
|
||||
</span>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
${this._triggers.length ||
|
||||
this._conditions.length ||
|
||||
this._actions.length
|
||||
? html`
|
||||
${this._triggers.length
|
||||
? html`
|
||||
<ha-device-triggers-card
|
||||
.hass=${this.hass}
|
||||
.automations=${this._triggers}
|
||||
.entityReg=${this._params.entityReg}
|
||||
></ha-device-triggers-card>
|
||||
`
|
||||
: ""}
|
||||
${this._conditions.length
|
||||
? html`
|
||||
<ha-device-conditions-card
|
||||
.hass=${this.hass}
|
||||
.automations=${this._conditions}
|
||||
.entityReg=${this._params.entityReg}
|
||||
></ha-device-conditions-card>
|
||||
`
|
||||
: ""}
|
||||
${this._actions.length
|
||||
? html`
|
||||
<ha-device-actions-card
|
||||
.hass=${this.hass}
|
||||
.automations=${this._actions}
|
||||
.script=${this._params.script}
|
||||
.entityReg=${this._params.entityReg}
|
||||
></ha-device-actions-card>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.devices.automation.no_device_automations"
|
||||
? html`<li divider role="separator"></li>`
|
||||
: nothing}
|
||||
<ha-list-item
|
||||
hasmeta
|
||||
twoline
|
||||
graphic="icon"
|
||||
@request-selected=${this._handleRowClick}
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPencilOutline}></ha-svg-icon>
|
||||
${this.hass.localize(`ui.panel.config.devices.${mode}.new.title`)}
|
||||
<span slot="secondary">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.devices.${mode}.new.description`
|
||||
)}
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
</span>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</mwc-list>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return haStyleDialog;
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
--mdc-dialog-max-height: 60vh;
|
||||
}
|
||||
@media all and (min-width: 550px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 500px;
|
||||
}
|
||||
}
|
||||
ha-icon-next {
|
||||
width: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,23 +0,0 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import {
|
||||
DeviceCondition,
|
||||
localizeDeviceAutomationCondition,
|
||||
} from "../../../../data/device_automation";
|
||||
import { HaDeviceAutomationCard } from "./ha-device-automation-card";
|
||||
|
||||
@customElement("ha-device-conditions-card")
|
||||
export class HaDeviceConditionsCard extends HaDeviceAutomationCard<DeviceCondition> {
|
||||
readonly type = "condition";
|
||||
|
||||
readonly headerKey = "ui.panel.config.devices.automation.conditions.caption";
|
||||
|
||||
constructor() {
|
||||
super(localizeDeviceAutomationCondition);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-device-conditions-card": HaDeviceConditionsCard;
|
||||
}
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import {
|
||||
DeviceTrigger,
|
||||
localizeDeviceAutomationTrigger,
|
||||
} from "../../../../data/device_automation";
|
||||
import { HaDeviceAutomationCard } from "./ha-device-automation-card";
|
||||
|
||||
@customElement("ha-device-triggers-card")
|
||||
export class HaDeviceTriggersCard extends HaDeviceAutomationCard<DeviceTrigger> {
|
||||
readonly type = "trigger";
|
||||
|
||||
readonly headerKey = "ui.panel.config.devices.automation.triggers.caption";
|
||||
|
||||
constructor() {
|
||||
super(localizeDeviceAutomationTrigger);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-device-triggers-card": HaDeviceTriggersCard;
|
||||
}
|
||||
}
|
@@ -35,6 +35,7 @@ import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import {
|
||||
ConfigEntry,
|
||||
@@ -1354,16 +1355,14 @@ export class HaConfigDevicePage extends LitElement {
|
||||
.filter((entity) => entity.newId)
|
||||
.map(
|
||||
(entity) =>
|
||||
html`<li style="white-space: nowrap;">
|
||||
${entity.oldId} -> ${entity.newId}
|
||||
</li>`
|
||||
html`<tr>
|
||||
<td>${entity.oldId}</td>
|
||||
<td>${entity.newId}</td>
|
||||
</tr>`
|
||||
);
|
||||
const dialogNoRenames = entityIdRenames
|
||||
.filter((entity) => !entity.newId)
|
||||
.map(
|
||||
(entity) =>
|
||||
html`<li style="white-space: nowrap;">${entity.oldId}</li>`
|
||||
);
|
||||
.map((entity) => html`<li>${entity.oldId}</li>`);
|
||||
|
||||
if (dialogRenames.length) {
|
||||
renameEntityid = await showConfirmationDialog(this, {
|
||||
@@ -1372,17 +1371,46 @@ export class HaConfigDevicePage extends LitElement {
|
||||
),
|
||||
text: html`${this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_entity_ids_warning"
|
||||
)} <br /><br />${this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_entity_will_rename"
|
||||
)}:
|
||||
${dialogRenames}
|
||||
)} <br /><br />
|
||||
<ha-expansion-panel outlined>
|
||||
<span slot="header"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_entity_will_rename",
|
||||
{ count: dialogRenames.length }
|
||||
)}</span
|
||||
>
|
||||
<div style="overflow: auto;">
|
||||
<table style="width: 100%; text-align: var(--float-start);">
|
||||
<tr>
|
||||
<th>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_old"
|
||||
)}
|
||||
</th>
|
||||
<th>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_new"
|
||||
)}
|
||||
</th>
|
||||
</tr>
|
||||
${dialogRenames}
|
||||
</table>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
${dialogNoRenames.length
|
||||
? html`<br /><br />${this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_entity_wont_rename",
|
||||
{ deviceSlug: oldDeviceSlug }
|
||||
)}:
|
||||
${dialogNoRenames}`
|
||||
: nothing}`,
|
||||
? html`<ha-expansion-panel outlined>
|
||||
<span slot="header"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_entity_wont_rename",
|
||||
{
|
||||
count: dialogNoRenames.length,
|
||||
deviceSlug: oldDeviceSlug,
|
||||
}
|
||||
)}</span
|
||||
>
|
||||
${dialogNoRenames}</ha-expansion-panel
|
||||
>`
|
||||
: nothing} `,
|
||||
confirmText: this.hass.localize("ui.common.rename"),
|
||||
dismissText: this.hass.localize("ui.common.no"),
|
||||
warning: true,
|
||||
@@ -1392,11 +1420,15 @@ export class HaConfigDevicePage extends LitElement {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_entity_no_renamable_entity_ids"
|
||||
),
|
||||
text: html`${this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_entity_wont_rename",
|
||||
{ deviceSlug: oldDeviceSlug }
|
||||
)}:
|
||||
${dialogNoRenames}`,
|
||||
text: html`<ha-expansion-panel outlined>
|
||||
<span slot="header"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_entity_wont_rename",
|
||||
{ deviceSlug: oldDeviceSlug, count: dialogNoRenames.length }
|
||||
)}</span
|
||||
>
|
||||
${dialogNoRenames}
|
||||
</ha-expansion-panel>`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -37,6 +37,7 @@ import {
|
||||
ThreadDataSet,
|
||||
ThreadRouter,
|
||||
addThreadDataSet,
|
||||
getThreadDataSetTLV,
|
||||
listThreadDataSets,
|
||||
removeThreadDataSet,
|
||||
setPreferredBorderAgent,
|
||||
@@ -168,8 +169,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
(otbr) => otbr.extended_pan_id === network.dataset!.extended_pan_id
|
||||
));
|
||||
const canImportKeychain =
|
||||
this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain &&
|
||||
otbrForNetwork;
|
||||
this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain;
|
||||
|
||||
return html`<ha-card>
|
||||
<div class="card-header">
|
||||
@@ -208,8 +208,12 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
${network.routers.map((router) => {
|
||||
const otbr =
|
||||
this._otbrInfo && this._otbrInfo[router.extended_address];
|
||||
const showOverflow =
|
||||
("dataset" in network && router.border_agent_id) || otbr;
|
||||
const showDefaultRouter = !!network.dataset;
|
||||
const isDefaultRouter =
|
||||
showDefaultRouter &&
|
||||
router.extended_address ===
|
||||
network.dataset!.preferred_extended_address;
|
||||
const showOverflow = showDefaultRouter || otbr;
|
||||
return html`<ha-list-item
|
||||
class="router"
|
||||
twoline
|
||||
@@ -235,9 +239,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
""}
|
||||
<span slot="secondary">${router.server}</span>
|
||||
${showOverflow
|
||||
? html`${network.dataset &&
|
||||
router.extended_address ===
|
||||
network.dataset.preferred_extended_address
|
||||
? html`${isDefaultRouter
|
||||
? html`<ha-svg-icon
|
||||
.path=${mdiCellphoneKey}
|
||||
.title=${this.hass.localize(
|
||||
@@ -259,13 +261,9 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
${network.dataset && router.border_agent_id
|
||||
? html`<ha-list-item
|
||||
.disabled=${router.border_agent_id ===
|
||||
network.dataset.preferred_border_agent_id}
|
||||
>
|
||||
${router.border_agent_id ===
|
||||
network.dataset.preferred_border_agent_id
|
||||
${showDefaultRouter
|
||||
? html`<ha-list-item .disabled=${isDefaultRouter}>
|
||||
${isDefaultRouter
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.thread.default_router"
|
||||
)
|
||||
@@ -321,9 +319,13 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
>
|
||||
</div>`
|
||||
: ""}
|
||||
${canImportKeychain
|
||||
${canImportKeychain &&
|
||||
network.dataset?.preferred &&
|
||||
network.routers?.length
|
||||
? html`<div class="card-actions">
|
||||
<mwc-button .otbr=${otbrForNetwork} @click=${this._sendCredentials}
|
||||
<mwc-button
|
||||
.networkDataset=${network.dataset}
|
||||
@click=${this._sendCredentials}
|
||||
>Send credentials to phone</mwc-button
|
||||
>
|
||||
</div>`
|
||||
@@ -331,17 +333,30 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
</ha-card>`;
|
||||
}
|
||||
|
||||
private _sendCredentials(ev) {
|
||||
const otbr = (ev.currentTarget as any).otbr as OTBRInfo;
|
||||
if (!otbr) {
|
||||
private async _sendCredentials(ev) {
|
||||
const dataset = (ev.currentTarget as any).networkDataset as ThreadDataSet;
|
||||
if (!dataset) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!dataset.preferred_extended_address &&
|
||||
!dataset.preferred_border_agent_id
|
||||
) {
|
||||
showAlertDialog(this, {
|
||||
title: "Error",
|
||||
text: this.hass.localize("ui.panel.config.thread.no_preferred_router"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "thread/store_in_platform_keychain",
|
||||
payload: {
|
||||
mac_extended_address: otbr.extended_address,
|
||||
border_agent_id: otbr.border_agent_id,
|
||||
active_operational_dataset: otbr.active_dataset_tlvs,
|
||||
mac_extended_address: dataset.preferred_extended_address,
|
||||
border_agent_id: dataset.preferred_border_agent_id,
|
||||
active_operational_dataset: (
|
||||
await getThreadDataSetTLV(this.hass, dataset.dataset_id)
|
||||
).tlv,
|
||||
extended_pan_id: dataset.extended_pan_id,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -467,10 +482,9 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
const network = (ev.currentTarget as any).network as ThreadNetwork;
|
||||
const router = (ev.currentTarget as any).router as ThreadRouter;
|
||||
const otbr = (ev.currentTarget as any).otbr as OTBRInfo;
|
||||
const index =
|
||||
network.dataset && router.border_agent_id
|
||||
? Number(ev.detail.index)
|
||||
: Number(ev.detail.index) + 1;
|
||||
const index = network.dataset
|
||||
? Number(ev.detail.index)
|
||||
: Number(ev.detail.index) + 1;
|
||||
switch (index) {
|
||||
case 0:
|
||||
this._setPreferredBorderAgent(network.dataset!, router);
|
||||
|
@@ -24,6 +24,7 @@ import { haStyle } from "../../../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../../../types";
|
||||
import { formatAsPaddedHex, sortZHAGroups } from "./functions";
|
||||
import { zhaTabs } from "./zha-config-dashboard";
|
||||
import { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
|
||||
export interface GroupRowData extends ZHAGroup {
|
||||
group?: GroupRowData;
|
||||
@@ -71,38 +72,35 @@ export class ZHAGroupsDashboard extends LitElement {
|
||||
});
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean): DataTableColumnContainer<GroupRowData> =>
|
||||
narrow
|
||||
? {
|
||||
name: {
|
||||
title: "Group",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
flex: 2,
|
||||
},
|
||||
}
|
||||
: {
|
||||
name: {
|
||||
title: this.hass.localize("ui.panel.config.zha.groups.groups"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
flex: 2,
|
||||
},
|
||||
group_id: {
|
||||
title: this.hass.localize("ui.panel.config.zha.groups.group_id"),
|
||||
type: "numeric",
|
||||
template: (group) => html` ${formatAsPaddedHex(group.group_id)} `,
|
||||
sortable: true,
|
||||
},
|
||||
members: {
|
||||
title: this.hass.localize("ui.panel.config.zha.groups.members"),
|
||||
type: "numeric",
|
||||
template: (group) => html` ${group.members.length} `,
|
||||
sortable: true,
|
||||
},
|
||||
}
|
||||
(localize: LocalizeFunc): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer<GroupRowData> = {
|
||||
name: {
|
||||
title: localize("ui.panel.config.zha.groups.groups"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
showNarrow: true,
|
||||
main: true,
|
||||
hideable: false,
|
||||
moveable: false,
|
||||
direction: "asc",
|
||||
flex: 2,
|
||||
},
|
||||
group_id: {
|
||||
title: localize("ui.panel.config.zha.groups.group_id"),
|
||||
type: "numeric",
|
||||
template: (group) => html` ${formatAsPaddedHex(group.group_id)} `,
|
||||
sortable: true,
|
||||
},
|
||||
members: {
|
||||
title: localize("ui.panel.config.zha.groups.members"),
|
||||
type: "numeric",
|
||||
template: (group) => html` ${group.members.length} `,
|
||||
sortable: true,
|
||||
},
|
||||
};
|
||||
|
||||
return columns;
|
||||
}
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -112,7 +110,7 @@ export class ZHAGroupsDashboard extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.columns=${this._columns(this.narrow)}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${this._formattedGroups(this._groups)}
|
||||
@row-click=${this._handleRowClicked}
|
||||
clickable
|
||||
|
@@ -18,6 +18,7 @@ import { showRepairsIssueDialog } from "./show-repair-issue-dialog";
|
||||
import {
|
||||
STATISTIC_TYPES,
|
||||
StatisticsValidationResult,
|
||||
updateStatisticsIssues,
|
||||
} from "../../../data/recorder";
|
||||
|
||||
@customElement("ha-config-repairs")
|
||||
@@ -144,25 +145,19 @@ class HaConfigRepairs extends LitElement {
|
||||
issue.translation_key &&
|
||||
STATISTIC_TYPES.includes(issue.translation_key as any)
|
||||
) {
|
||||
const localize =
|
||||
await this.hass.loadFragmentTranslation("developer-tools");
|
||||
this.hass.loadFragmentTranslation("developer-tools");
|
||||
const data = await fetchRepairsIssueData(
|
||||
this.hass.connection,
|
||||
issue.domain,
|
||||
issue.issue_id
|
||||
);
|
||||
if ("issue_type" in data.issue_data) {
|
||||
await fixStatisticsIssue(
|
||||
this,
|
||||
this.hass,
|
||||
localize || this.hass.localize,
|
||||
{
|
||||
type: data.issue_data
|
||||
.issue_type as StatisticsValidationResult["type"],
|
||||
data: data.issue_data as any,
|
||||
}
|
||||
);
|
||||
this.hass.callWS({ type: "recorder/update_statistics_issues" });
|
||||
await fixStatisticsIssue(this, {
|
||||
type: data.issue_data
|
||||
.issue_type as StatisticsValidationResult["type"],
|
||||
data: data.issue_data as any,
|
||||
});
|
||||
updateStatisticsIssues(this.hass);
|
||||
}
|
||||
} else {
|
||||
showRepairsIssueDialog(this, {
|
||||
|
@@ -47,7 +47,7 @@ export const showRepairsFlowDialog = (
|
||||
},
|
||||
{
|
||||
flowType: "repair_flow",
|
||||
loadDevicesAndAreas: false,
|
||||
showDevices: false,
|
||||
createFlow: async (hass, handler) => {
|
||||
const [step] = await Promise.all([
|
||||
createRepairsFlow(hass, handler, issue.issue_id),
|
||||
|
@@ -1,8 +1,20 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
||||
import {
|
||||
extractSearchParam,
|
||||
removeSearchParam,
|
||||
} from "../../../common/url/search-params";
|
||||
import { nestedArrayMove } from "../../../common/util/array-move";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
@@ -12,6 +24,7 @@ import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import "../automation/action/ha-automation-action";
|
||||
import type HaAutomationAction from "../automation/action/ha-automation-action";
|
||||
import "./ha-script-fields";
|
||||
import type HaScriptFields from "./ha-script-fields";
|
||||
|
||||
@@ -58,6 +71,31 @@ export class HaManualScriptEditor extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
const expanded = extractSearchParam("expanded");
|
||||
if (expanded === "1") {
|
||||
this._clearParam("expanded");
|
||||
const items = this.shadowRoot!.querySelectorAll<HaAutomationAction>(
|
||||
"ha-automation-action"
|
||||
);
|
||||
|
||||
items.forEach((el) => {
|
||||
el.updateComplete.then(() => {
|
||||
el.expandAll();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _clearParam(param: string) {
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
constructUrlCurrentPath(removeSearchParam(param))
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.config.description
|
||||
|
@@ -7,6 +7,7 @@ import "../../../components/ha-card";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/ha-yaml-editor";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-alert";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("event-subscribe-card")
|
||||
@@ -22,6 +23,8 @@ class EventSubscribeCard extends LitElement {
|
||||
event: HassEvent;
|
||||
}> = [];
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _eventCount = 0;
|
||||
|
||||
public disconnectedCallback() {
|
||||
@@ -52,6 +55,9 @@ class EventSubscribeCard extends LitElement {
|
||||
.value=${this._eventType}
|
||||
@input=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button
|
||||
@@ -110,33 +116,43 @@ class EventSubscribeCard extends LitElement {
|
||||
|
||||
private _valueChanged(ev): void {
|
||||
this._eventType = ev.target.value;
|
||||
this._error = undefined;
|
||||
}
|
||||
|
||||
private async _startOrStopListening(): Promise<void> {
|
||||
if (this._subscribed) {
|
||||
this._subscribed();
|
||||
this._subscribed = undefined;
|
||||
this._error = undefined;
|
||||
} else {
|
||||
this._subscribed = await this.hass!.connection.subscribeEvents<HassEvent>(
|
||||
(event) => {
|
||||
const tail =
|
||||
this._events.length > 30 ? this._events.slice(0, 29) : this._events;
|
||||
this._events = [
|
||||
{
|
||||
event,
|
||||
id: this._eventCount++,
|
||||
},
|
||||
...tail,
|
||||
];
|
||||
},
|
||||
this._eventType
|
||||
);
|
||||
try {
|
||||
this._subscribed =
|
||||
await this.hass!.connection.subscribeEvents<HassEvent>((event) => {
|
||||
const tail =
|
||||
this._events.length > 30
|
||||
? this._events.slice(0, 29)
|
||||
: this._events;
|
||||
this._events = [
|
||||
{
|
||||
event,
|
||||
id: this._eventCount++,
|
||||
},
|
||||
...tail,
|
||||
];
|
||||
}, this._eventType);
|
||||
} catch (error: any) {
|
||||
this._error = this.hass!.localize(
|
||||
"ui.panel.developer-tools.tabs.events.subscribe_failed",
|
||||
{ error: error.message || "Unknown error" }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _clearEvents(): void {
|
||||
this._events = [];
|
||||
this._eventCount = 0;
|
||||
this._error = undefined;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -145,6 +161,9 @@ class EventSubscribeCard extends LitElement {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.error-message {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.event {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
padding-top: 8px;
|
||||
|
@@ -1,24 +1,51 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiSlopeUphill } from "@mdi/js";
|
||||
import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiClose,
|
||||
mdiCog,
|
||||
mdiFormatListChecks,
|
||||
mdiMenuDown,
|
||||
mdiSlopeUphill,
|
||||
mdiUnfoldLessHorizontal,
|
||||
mdiUnfoldMoreHorizontal,
|
||||
} from "@mdi/js";
|
||||
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/chips/ha-assist-chip";
|
||||
import "../../../components/data-table/ha-data-table";
|
||||
import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
HaDataTable,
|
||||
SelectionChangedEvent,
|
||||
SortingDirection,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import { showDataTableSettingsDialog } from "../../../components/data-table/show-dialog-data-table-settings";
|
||||
import "../../../components/ha-md-button-menu";
|
||||
import "../../../components/ha-dialog";
|
||||
import { HaMenu } from "../../../components/ha-menu";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import "../../../components/search-input-outlined";
|
||||
import { subscribeEntityRegistry } from "../../../data/entity_registry";
|
||||
import {
|
||||
getStatisticIds,
|
||||
StatisticsMetaData,
|
||||
StatisticsValidationResult,
|
||||
clearStatistics,
|
||||
getStatisticIds,
|
||||
updateStatisticsIssues,
|
||||
validateStatistics,
|
||||
} from "../../../data/recorder";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { showConfirmationDialog } from "../../lovelace/custom-card-helpers";
|
||||
import { fixStatisticsIssue } from "./fix-statistics";
|
||||
import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum";
|
||||
|
||||
@@ -30,9 +57,17 @@ const FIX_ISSUES_ORDER = {
|
||||
units_changed: 3,
|
||||
};
|
||||
|
||||
const FIXABLE_ISSUES = [
|
||||
"no_state",
|
||||
"entity_no_longer_recorded",
|
||||
"unsupported_state_class",
|
||||
"units_changed",
|
||||
];
|
||||
|
||||
type StatisticData = StatisticsMetaData & {
|
||||
issues?: StatisticsValidationResult[];
|
||||
state?: HassEntity;
|
||||
selectable?: boolean;
|
||||
};
|
||||
|
||||
type DisplayedStatisticData = StatisticData & {
|
||||
@@ -48,9 +83,39 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _data: StatisticData[] = [] as StatisticsMetaData[];
|
||||
|
||||
@state() private filter = "";
|
||||
|
||||
@state() private _selected: string[] = [];
|
||||
|
||||
@state() private groupOrder?: string[];
|
||||
|
||||
@state() private columnOrder?: string[];
|
||||
|
||||
@state() private hiddenColumns?: string[];
|
||||
|
||||
@state() private _sortColumn?: string;
|
||||
|
||||
@state() private _sortDirection: SortingDirection = null;
|
||||
|
||||
@state() private _groupColumn?: string;
|
||||
|
||||
@state() private _selectMode = false;
|
||||
|
||||
@query("ha-data-table", true) private _dataTable!: HaDataTable;
|
||||
|
||||
@query("#group-by-menu") private _groupByMenu!: HaMenu;
|
||||
|
||||
@query("#sort-by-menu") private _sortByMenu!: HaMenu;
|
||||
|
||||
private _disabledEntities = new Set<string>();
|
||||
|
||||
private _deletedStatistics = new Set<string>();
|
||||
private _toggleGroupBy() {
|
||||
this._groupByMenu.open = !this._groupByMenu.open;
|
||||
}
|
||||
|
||||
private _toggleSortBy() {
|
||||
this._sortByMenu.open = !this._sortByMenu.open;
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
this._validateStatistics();
|
||||
@@ -110,6 +175,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
},
|
||||
issues_string: {
|
||||
title: localize(
|
||||
@@ -117,6 +183,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
direction: "asc",
|
||||
flex: 2,
|
||||
template: (statistic) =>
|
||||
@@ -135,7 +202,11 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
.data=${statistic.issues}
|
||||
>
|
||||
${localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.fix"
|
||||
statistic.issues.some((issue) =>
|
||||
FIXABLE_ISSUES.includes(issue.type)
|
||||
)
|
||||
? "ui.panel.developer-tools.tabs.statistics.fix_issue.fix"
|
||||
: "ui.panel.developer-tools.tabs.statistics.fix_issue.info"
|
||||
)}
|
||||
</mwc-button>`
|
||||
: "—"}`,
|
||||
@@ -166,22 +237,367 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const localize = this.hass.localize;
|
||||
const columns = this._columns(this.hass.localize);
|
||||
|
||||
const selectModeBtn = !this._selectMode
|
||||
? html`<ha-assist-chip
|
||||
class="has-dropdown select-mode-chip"
|
||||
.active=${this._selectMode}
|
||||
@click=${this._enableSelectMode}
|
||||
.title=${localize(
|
||||
"ui.components.subpage-data-table.enter_selection_mode"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiFormatListChecks}></ha-svg-icon>
|
||||
</ha-assist-chip> `
|
||||
: nothing;
|
||||
|
||||
const searchBar = html`<search-input-outlined
|
||||
.hass=${this.hass}
|
||||
.filter=${this.filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>`;
|
||||
|
||||
const sortByMenu = Object.values(columns).find((col) => col.sortable)
|
||||
? html`
|
||||
<ha-assist-chip
|
||||
.label=${localize("ui.components.subpage-data-table.sort_by", {
|
||||
sortColumn: this._sortColumn
|
||||
? ` ${columns[this._sortColumn]?.title || columns[this._sortColumn]?.label}` ||
|
||||
""
|
||||
: "",
|
||||
})}
|
||||
id="sort-by-anchor"
|
||||
@click=${this._toggleSortBy}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`
|
||||
: nothing;
|
||||
|
||||
const groupByMenu = Object.values(columns).find((col) => col.groupable)
|
||||
? html`
|
||||
<ha-assist-chip
|
||||
.label=${localize("ui.components.subpage-data-table.group_by", {
|
||||
groupColumn: this._groupColumn
|
||||
? ` ${columns[this._groupColumn].title || columns[this._groupColumn].label}`
|
||||
: "",
|
||||
})}
|
||||
id="group-by-anchor"
|
||||
@click=${this._toggleGroupBy}
|
||||
>
|
||||
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon
|
||||
></ha-assist-chip>
|
||||
`
|
||||
: nothing;
|
||||
|
||||
const settingsButton = html`<ha-assist-chip
|
||||
class="has-dropdown select-mode-chip"
|
||||
@click=${this._openSettings}
|
||||
.title=${localize("ui.components.subpage-data-table.settings")}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
|
||||
</ha-assist-chip>`;
|
||||
|
||||
return html`
|
||||
<ha-data-table
|
||||
.hass=${this.hass}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${this._displayData(this._data, this.hass.localize)}
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.data_table.no_statistics"
|
||||
<div>
|
||||
${this._selectMode
|
||||
? html`<div class="selection-bar">
|
||||
<div class="selection-controls">
|
||||
<ha-icon-button
|
||||
.path=${mdiClose}
|
||||
@click=${this._disableSelectMode}
|
||||
.label=${localize(
|
||||
"ui.components.subpage-data-table.exit_selection_mode"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
<ha-md-button-menu positioning="absolute">
|
||||
<ha-assist-chip
|
||||
.label=${localize(
|
||||
"ui.components.subpage-data-table.select"
|
||||
)}
|
||||
slot="trigger"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiFormatListChecks}
|
||||
></ha-svg-icon>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon
|
||||
></ha-assist-chip>
|
||||
<ha-md-menu-item
|
||||
.value=${undefined}
|
||||
@click=${this._selectAll}
|
||||
>
|
||||
<div slot="headline">
|
||||
${localize("ui.components.subpage-data-table.select_all")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.value=${undefined}
|
||||
@click=${this._selectAllIssues}
|
||||
>
|
||||
<div slot="headline">
|
||||
${localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.data_table.select_all_issues"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.value=${undefined}
|
||||
@click=${this._selectNone}
|
||||
>
|
||||
<div slot="headline">
|
||||
${localize(
|
||||
"ui.components.subpage-data-table.select_none"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<md-divider role="separator" tabindex="-1"></md-divider>
|
||||
<ha-md-menu-item
|
||||
.value=${undefined}
|
||||
@click=${this._disableSelectMode}
|
||||
>
|
||||
<div slot="headline">
|
||||
${localize(
|
||||
"ui.components.subpage-data-table.close_select_mode"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
<p>
|
||||
${localize("ui.components.subpage-data-table.selected", {
|
||||
selected: this._selected.length,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div class="center-vertical">
|
||||
<slot name="selection-bar"></slot>
|
||||
</div>
|
||||
<ha-assist-chip
|
||||
.label=${localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.delete_selected"
|
||||
)}
|
||||
.disabled=${!this._selected.length}
|
||||
@click=${this._clearSelected}
|
||||
>
|
||||
</ha-assist-chip>
|
||||
</div>`
|
||||
: nothing}
|
||||
<div slot="toolbar-icon">
|
||||
<slot name="toolbar-icon"></slot>
|
||||
</div>
|
||||
${this.narrow
|
||||
? html`
|
||||
<div slot="header">
|
||||
<slot name="header">
|
||||
<div class="search-toolbar">${searchBar}</div>
|
||||
</slot>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<ha-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.columns=${columns}
|
||||
.data=${this._displayData(this._data, this.hass.localize)}
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.data_table.no_statistics"
|
||||
)}
|
||||
.filter=${this.filter}
|
||||
.selectable=${this._selectMode}
|
||||
id="statistic_id"
|
||||
clickable
|
||||
.sortColumn=${this._sortColumn}
|
||||
.sortDirection=${this._sortDirection}
|
||||
.groupColumn=${this._groupColumn}
|
||||
.groupOrder=${this.groupOrder}
|
||||
.columnOrder=${this.columnOrder}
|
||||
.hiddenColumns=${this.hiddenColumns}
|
||||
@row-click=${this._rowClicked}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
>
|
||||
${!this.narrow
|
||||
? html`
|
||||
<div slot="header">
|
||||
<slot name="header">
|
||||
<div class="table-header">
|
||||
${selectModeBtn}${searchBar}${groupByMenu}${sortByMenu}${settingsButton}
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
`
|
||||
: html`<div slot="header"></div>
|
||||
<div slot="header-row" class="narrow-header-row">
|
||||
${selectModeBtn}${groupByMenu}${sortByMenu}${settingsButton}
|
||||
</div>`}
|
||||
</ha-data-table>
|
||||
</div>
|
||||
<ha-menu anchor="group-by-anchor" id="group-by-menu" positioning="fixed">
|
||||
${Object.entries(columns).map(([id, column]) =>
|
||||
column.groupable
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
.value=${id}
|
||||
@click=${this._handleGroupBy}
|
||||
.selected=${id === this._groupColumn}
|
||||
class=${classMap({ selected: id === this._groupColumn })}
|
||||
>
|
||||
${column.title || column.label}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
.narrow=${this.narrow}
|
||||
id="statistic_id"
|
||||
clickable
|
||||
@row-click=${this._rowClicked}
|
||||
></ha-data-table>
|
||||
<ha-md-menu-item
|
||||
.value=${undefined}
|
||||
@click=${this._handleGroupBy}
|
||||
.selected=${this._groupColumn === undefined}
|
||||
class=${classMap({ selected: this._groupColumn === undefined })}
|
||||
>
|
||||
${localize("ui.components.subpage-data-table.dont_group_by")}
|
||||
</ha-md-menu-item>
|
||||
<md-divider role="separator" tabindex="-1"></md-divider>
|
||||
<ha-md-menu-item
|
||||
@click=${this._collapseAllGroups}
|
||||
.disabled=${this._groupColumn === undefined}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiUnfoldLessHorizontal}
|
||||
></ha-svg-icon>
|
||||
${localize("ui.components.subpage-data-table.collapse_all_groups")}
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
@click=${this._expandAllGroups}
|
||||
.disabled=${this._groupColumn === undefined}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiUnfoldMoreHorizontal}
|
||||
></ha-svg-icon>
|
||||
${localize("ui.components.subpage-data-table.expand_all_groups")}
|
||||
</ha-md-menu-item>
|
||||
</ha-menu>
|
||||
<ha-menu anchor="sort-by-anchor" id="sort-by-menu" positioning="fixed">
|
||||
${Object.entries(columns).map(([id, column]) =>
|
||||
column.sortable
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
.value=${id}
|
||||
@click=${this._handleSortBy}
|
||||
keep-open
|
||||
.selected=${id === this._sortColumn}
|
||||
class=${classMap({ selected: id === this._sortColumn })}
|
||||
>
|
||||
${this._sortColumn === id
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${this._sortDirection === "desc"
|
||||
? mdiArrowDown
|
||||
: mdiArrowUp}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
${column.title || column.label}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
</ha-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
if (this.filter === ev.detail.value) {
|
||||
return;
|
||||
}
|
||||
this.filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleSelectionChanged(
|
||||
ev: HASSDomEvent<SelectionChangedEvent>
|
||||
): void {
|
||||
this._selected = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleSortBy(ev) {
|
||||
const columnId = ev.currentTarget.value;
|
||||
if (!this._sortDirection || this._sortColumn !== columnId) {
|
||||
this._sortDirection = "asc";
|
||||
} else if (this._sortDirection === "asc") {
|
||||
this._sortDirection = "desc";
|
||||
} else {
|
||||
this._sortDirection = null;
|
||||
}
|
||||
this._sortColumn = this._sortDirection === null ? undefined : columnId;
|
||||
}
|
||||
|
||||
private _handleGroupBy(ev) {
|
||||
this._setGroupColumn(ev.currentTarget.value);
|
||||
}
|
||||
|
||||
private _setGroupColumn(columnId: string) {
|
||||
this._groupColumn = columnId;
|
||||
}
|
||||
|
||||
private _openSettings() {
|
||||
showDataTableSettingsDialog(this, {
|
||||
columns: this._columns(this.hass.localize),
|
||||
hiddenColumns: this.hiddenColumns,
|
||||
columnOrder: this.columnOrder,
|
||||
onUpdate: (
|
||||
columnOrder: string[] | undefined,
|
||||
hiddenColumns: string[] | undefined
|
||||
) => {
|
||||
this.columnOrder = columnOrder;
|
||||
this.hiddenColumns = hiddenColumns;
|
||||
},
|
||||
localizeFunc: this.hass.localize,
|
||||
});
|
||||
}
|
||||
|
||||
private _collapseAllGroups() {
|
||||
this._dataTable.collapseAllGroups();
|
||||
}
|
||||
|
||||
private _expandAllGroups() {
|
||||
this._dataTable.expandAllGroups();
|
||||
}
|
||||
|
||||
private _enableSelectMode() {
|
||||
this._selectMode = true;
|
||||
}
|
||||
|
||||
private _disableSelectMode() {
|
||||
this._selectMode = false;
|
||||
this._dataTable.clearSelection();
|
||||
}
|
||||
|
||||
private _selectAll() {
|
||||
this._dataTable.selectAll();
|
||||
}
|
||||
|
||||
private _selectNone() {
|
||||
this._dataTable.clearSelection();
|
||||
}
|
||||
|
||||
private _selectAllIssues() {
|
||||
this._dataTable.select(
|
||||
this._data
|
||||
.filter((statistic) => statistic.issues)
|
||||
.map((statistic) => statistic.statistic_id),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
private _showStatisticsAdjustSumDialog(ev) {
|
||||
ev.stopPropagation();
|
||||
showStatisticsAdjustSumDialog(this, {
|
||||
@@ -221,13 +637,13 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
validateStatistics(this.hass),
|
||||
]);
|
||||
|
||||
updateStatisticsIssues(this.hass);
|
||||
|
||||
const statsIds = new Set();
|
||||
|
||||
this._data = statisticIds
|
||||
.filter(
|
||||
(statistic) =>
|
||||
!this._disabledEntities.has(statistic.statistic_id) &&
|
||||
!this._deletedStatistics.has(statistic.statistic_id)
|
||||
(statistic) => !this._disabledEntities.has(statistic.statistic_id)
|
||||
)
|
||||
.map((statistic) => {
|
||||
statsIds.add(statistic.statistic_id);
|
||||
@@ -241,8 +657,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
Object.keys(issues).forEach((statisticId) => {
|
||||
if (
|
||||
!statsIds.has(statisticId) &&
|
||||
!this._disabledEntities.has(statisticId) &&
|
||||
!this._deletedStatistics.has(statisticId)
|
||||
!this._disabledEntities.has(statisticId)
|
||||
) {
|
||||
this._data.push({
|
||||
statistic_id: statisticId,
|
||||
@@ -258,6 +673,31 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _clearSelected = async () => {
|
||||
if (!this._selected.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deletableIds = this._selected;
|
||||
|
||||
await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.multi_delete.title"
|
||||
),
|
||||
text: html`${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.multi_delete.info_text",
|
||||
{ statistic_count: deletableIds.length }
|
||||
)}`,
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
confirm: async () => {
|
||||
await clearStatistics(this.hass, deletableIds);
|
||||
this._validateStatistics();
|
||||
this._dataTable.clearSelection();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private _fixIssue = async (ev) => {
|
||||
const issues = (ev.currentTarget.data as StatisticsValidationResult[]).sort(
|
||||
(itemA, itemB) =>
|
||||
@@ -265,25 +705,130 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
(FIX_ISSUES_ORDER[itemB.type] ?? 99)
|
||||
);
|
||||
const issue = issues[0];
|
||||
const result = await fixStatisticsIssue(
|
||||
this,
|
||||
this.hass,
|
||||
this.hass.localize,
|
||||
issue
|
||||
);
|
||||
if (
|
||||
result &&
|
||||
["no_state", "entity_no_longer_recorded", "state_class_removed"].includes(
|
||||
issue.type
|
||||
)
|
||||
) {
|
||||
this._deletedStatistics.add(issue.data.statistic_id);
|
||||
}
|
||||
await fixStatisticsIssue(this, issue);
|
||||
this._validateStatistics();
|
||||
};
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return haStyle;
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
ha-data-table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
--data-table-border-width: 0;
|
||||
}
|
||||
:host(:not([narrow])) ha-data-table {
|
||||
height: calc(100vh - 1px - var(--header-height));
|
||||
display: block;
|
||||
}
|
||||
|
||||
:host([narrow]) {
|
||||
--expansion-panel-summary-padding: 0 16px;
|
||||
}
|
||||
.table-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
--mdc-shape-small: 0;
|
||||
height: 56px;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
gap: 16px;
|
||||
box-sizing: border-box;
|
||||
background: var(--primary-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
search-input-outlined {
|
||||
flex: 1;
|
||||
}
|
||||
.search-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.narrow-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 0 16px;
|
||||
overflow-x: scroll;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.selection-bar {
|
||||
background: rgba(var(--rgb-primary-color), 0.1);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
--ha-assist-chip-container-color: var(--card-background-color);
|
||||
}
|
||||
|
||||
.selection-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.selection-controls p {
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
|
||||
.center-vertical {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
--ha-assist-chip-container-color: var(--card-background-color);
|
||||
}
|
||||
|
||||
.select-mode-chip {
|
||||
--md-assist-chip-icon-label-space: 0;
|
||||
--md-assist-chip-trailing-space: 8px;
|
||||
}
|
||||
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: calc(
|
||||
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
|
||||
);
|
||||
--mdc-dialog-max-width: calc(
|
||||
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
|
||||
);
|
||||
--mdc-dialog-min-height: 100%;
|
||||
--mdc-dialog-max-height: 100%;
|
||||
--vertical-align-dialog: flex-end;
|
||||
--ha-dialog-border-radius: 0;
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
#sort-by-anchor,
|
||||
#group-by-anchor,
|
||||
ha-button-menu-new ha-assist-chip {
|
||||
--md-assist-chip-trailing-space: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
201
src/panels/developer-tools/statistics/dialog-statistics-fix.ts
Normal file
201
src/panels/developer-tools/statistics/dialog-statistics-fix.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-dialog";
|
||||
import { clearStatistics, getStatisticLabel } from "../../../data/recorder";
|
||||
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import type { DialogStatisticsFixParams } from "./show-dialog-statistics-fix";
|
||||
import { showAlertDialog } from "../../lovelace/custom-card-helpers";
|
||||
|
||||
@customElement("dialog-statistics-fix")
|
||||
export class DialogStatisticsFix extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: DialogStatisticsFixParams;
|
||||
|
||||
@state() private _clearing = false;
|
||||
|
||||
public showDialog(params: DialogStatisticsFixParams): void {
|
||||
this._params = params;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._cancel();
|
||||
}
|
||||
|
||||
private _closeDialog(): void {
|
||||
this._params = undefined;
|
||||
this._clearing = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const issue = this._params.issue;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
@closed=${this._closeDialog}
|
||||
.heading=${this.hass.localize(
|
||||
`ui.panel.developer-tools.tabs.statistics.fix_issue.${issue.type}.title`
|
||||
)}
|
||||
>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
`ui.panel.developer-tools.tabs.statistics.fix_issue.${issue.type}.info_text_1`,
|
||||
{
|
||||
name: getStatisticLabel(
|
||||
this.hass,
|
||||
this._params.issue.data.statistic_id,
|
||||
undefined
|
||||
),
|
||||
statistic_id: this._params.issue.data.statistic_id,
|
||||
}
|
||||
)}<br /><br />
|
||||
${this.hass.localize(
|
||||
`ui.panel.developer-tools.tabs.statistics.fix_issue.${issue.type}.info_text_2`,
|
||||
{ statistic_id: issue.data.statistic_id }
|
||||
)}
|
||||
${issue.type === "entity_not_recorded"
|
||||
? html`<br /><br />
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/integrations/recorder/#configure-filter"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.info_text_3_link"
|
||||
)}</a
|
||||
>`
|
||||
: issue.type === "entity_no_longer_recorded"
|
||||
? html`<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/integrations/recorder/#configure-filter"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
${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"
|
||||
)}`
|
||||
: issue.type === "state_class_removed"
|
||||
? html`<ul>
|
||||
<li>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_3"
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_4"
|
||||
)}
|
||||
<a
|
||||
href="https://developers.home-assistant.io/docs/core/entity/sensor/#long-term-statistics"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_4_link"
|
||||
)}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_5"
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_6",
|
||||
{ statistic_id: issue.data.statistic_id }
|
||||
)}`
|
||||
: nothing}
|
||||
</p>
|
||||
|
||||
${issue.type !== "entity_not_recorded"
|
||||
? html`<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._clearStatistics}
|
||||
class="warning"
|
||||
.disabled=${this._clearing}
|
||||
>
|
||||
${this._clearing
|
||||
? html`<ha-circular-progress
|
||||
indeterminate
|
||||
size="small"
|
||||
aria-label="Saving"
|
||||
></ha-circular-progress>`
|
||||
: nothing}
|
||||
${this.hass.localize("ui.common.delete")}
|
||||
</mwc-button>
|
||||
<mwc-button slot="secondaryAction" @click=${this._cancel}>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>`
|
||||
: html`<mwc-button slot="primaryAction" @click=${this._cancel}>
|
||||
${this.hass.localize("ui.common.ok")}
|
||||
</mwc-button>`}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _cancel(): void {
|
||||
this._params?.cancelCallback!();
|
||||
this._closeDialog();
|
||||
}
|
||||
|
||||
private async _clearStatistics(): Promise<void> {
|
||||
this._clearing = true;
|
||||
try {
|
||||
await clearStatistics(this.hass, [this._params!.issue.data.statistic_id]);
|
||||
} catch (err: any) {
|
||||
await showAlertDialog(this, {
|
||||
title:
|
||||
err.code === "timeout"
|
||||
? this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.clearing_timeout_title"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.clearing_failed"
|
||||
),
|
||||
text:
|
||||
err.code === "timeout"
|
||||
? this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.clearing_timeout_text"
|
||||
)
|
||||
: err.message,
|
||||
});
|
||||
} finally {
|
||||
this._clearing = false;
|
||||
this._params?.fixedCallback!();
|
||||
this._closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [haStyle, haStyleDialog];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-statistics-fix": DialogStatisticsFix;
|
||||
}
|
||||
}
|
@@ -1,171 +1,17 @@
|
||||
import { html } from "lit";
|
||||
import {
|
||||
clearStatistics,
|
||||
getStatisticLabel,
|
||||
StatisticsValidationResult,
|
||||
} from "../../../data/recorder";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import {
|
||||
showConfirmationDialog,
|
||||
showAlertDialog,
|
||||
} from "../../lovelace/custom-card-helpers";
|
||||
import { StatisticsValidationResult } from "../../../data/recorder";
|
||||
import { showFixStatisticsDialog } from "./show-dialog-statistics-fix";
|
||||
import { showFixStatisticsUnitsChangedDialog } from "./show-dialog-statistics-fix-units-changed";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
export const fixStatisticsIssue = async (
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
localize: LocalizeFunc,
|
||||
issue: StatisticsValidationResult
|
||||
) => {
|
||||
switch (issue.type) {
|
||||
case "no_state":
|
||||
return showConfirmationDialog(element, {
|
||||
title: localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.no_state.title"
|
||||
),
|
||||
text: html`${localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.no_state.info_text_1",
|
||||
{
|
||||
name: getStatisticLabel(hass, issue.data.statistic_id, undefined),
|
||||
statistic_id: issue.data.statistic_id,
|
||||
}
|
||||
)}<br /><br />${localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.no_state.info_text_2",
|
||||
{ statistic_id: issue.data.statistic_id }
|
||||
)}`,
|
||||
confirmText: localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
confirm: async () => {
|
||||
await clearStatistics(hass, [issue.data.statistic_id]);
|
||||
},
|
||||
});
|
||||
case "entity_not_recorded":
|
||||
return showAlertDialog(element, {
|
||||
title: localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.title"
|
||||
),
|
||||
text: html`${localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.info_text_1",
|
||||
{
|
||||
name: getStatisticLabel(hass, issue.data.statistic_id, undefined),
|
||||
}
|
||||
)}<br /><br />${localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.info_text_2"
|
||||
)}<br /><br />
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
hass,
|
||||
"/integrations/recorder/#configure-filter"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
${localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.info_text_3_link"
|
||||
)}</a
|
||||
>`,
|
||||
});
|
||||
case "entity_no_longer_recorded":
|
||||
return showConfirmationDialog(element, {
|
||||
title: localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.title"
|
||||
),
|
||||
text: html`${localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_1",
|
||||
{
|
||||
name: getStatisticLabel(hass, issue.data.statistic_id, undefined),
|
||||
statistic_id: issue.data.statistic_id,
|
||||
}
|
||||
)}
|
||||
${localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_2"
|
||||
)}
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
hass,
|
||||
"/integrations/recorder/#configure-filter"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
${localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_3_link"
|
||||
)}</a
|
||||
><br /><br />
|
||||
${localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_4"
|
||||
)}`,
|
||||
confirmText: localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
confirm: async () => {
|
||||
await clearStatistics(hass, [issue.data.statistic_id]);
|
||||
},
|
||||
});
|
||||
case "state_class_removed":
|
||||
return showConfirmationDialog(element, {
|
||||
title: localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.title"
|
||||
),
|
||||
text: html`${localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_1",
|
||||
{
|
||||
name: getStatisticLabel(hass, issue.data.statistic_id, undefined),
|
||||
statistic_id: issue.data.statistic_id,
|
||||
}
|
||||
)}<br /><br />
|
||||
${localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_2"
|
||||
)}
|
||||
<ul>
|
||||
<li>
|
||||
${localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_3"
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
${localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_4"
|
||||
)}
|
||||
<a
|
||||
href="https://developers.home-assistant.io/docs/core/entity/sensor/#long-term-statistics"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
${localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_4_link"
|
||||
)}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
${localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_5"
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
${localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_6",
|
||||
{ statistic_id: issue.data.statistic_id }
|
||||
)}`,
|
||||
confirmText: localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
confirm: async () => {
|
||||
await clearStatistics(hass, [issue.data.statistic_id]);
|
||||
},
|
||||
});
|
||||
case "units_changed":
|
||||
return showFixStatisticsUnitsChangedDialog(element, {
|
||||
issue,
|
||||
});
|
||||
default:
|
||||
return showAlertDialog(element, {
|
||||
title: localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.no_support.title"
|
||||
),
|
||||
text: localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.no_support.info_text_1"
|
||||
),
|
||||
});
|
||||
if (issue.type === "units_changed") {
|
||||
return showFixStatisticsUnitsChangedDialog(element, {
|
||||
issue,
|
||||
});
|
||||
}
|
||||
return showFixStatisticsDialog(element, {
|
||||
issue,
|
||||
});
|
||||
};
|
||||
|
@@ -0,0 +1,33 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { StatisticsValidationResult } from "../../../data/recorder";
|
||||
|
||||
export const loadFixDialog = () => import("./dialog-statistics-fix");
|
||||
|
||||
export interface DialogStatisticsFixParams {
|
||||
issue: StatisticsValidationResult;
|
||||
fixedCallback?: () => void;
|
||||
cancelCallback?: () => void;
|
||||
}
|
||||
|
||||
export const showFixStatisticsDialog = (
|
||||
element: HTMLElement,
|
||||
detailParams: DialogStatisticsFixParams
|
||||
) =>
|
||||
new Promise((resolve) => {
|
||||
const origCallback = detailParams.fixedCallback;
|
||||
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-statistics-fix",
|
||||
dialogImport: loadFixDialog,
|
||||
dialogParams: {
|
||||
...detailParams,
|
||||
cancelCallback: () => {
|
||||
resolve(false);
|
||||
},
|
||||
fixedCallback: () => {
|
||||
resolve(true);
|
||||
origCallback?.();
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
@@ -8,6 +8,7 @@ import {
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-sortable";
|
||||
@@ -124,7 +125,7 @@ export class HuiViewBadges extends LitElement {
|
||||
.options=${BADGE_SORTABLE_OPTIONS}
|
||||
invert-swap
|
||||
>
|
||||
<div class="badges">
|
||||
<div class="badges ${classMap({ "edit-mode": editMode })}">
|
||||
${repeat(
|
||||
badges,
|
||||
(badge) => this._getBadgeKey(badge),
|
||||
@@ -185,6 +186,8 @@ export class HuiViewBadges extends LitElement {
|
||||
hui-badge-edit-mode {
|
||||
display: block;
|
||||
position: relative;
|
||||
min-width: 36px;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.add {
|
||||
|
@@ -129,6 +129,8 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
return css`
|
||||
ha-card {
|
||||
background: none;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
|
@@ -64,11 +64,16 @@ const addEntities = (entities: Set<string>, obj) => {
|
||||
if (obj.badges && Array.isArray(obj.badges)) {
|
||||
obj.badges.forEach((badge) => addEntityId(entities, badge));
|
||||
}
|
||||
if (obj.sections && Array.isArray(obj.sections)) {
|
||||
obj.sections.forEach((section) => addEntities(entities, section));
|
||||
}
|
||||
};
|
||||
|
||||
export const computeUsedEntities = (config: LovelaceConfig): Set<string> => {
|
||||
const entities = new Set<string>();
|
||||
config.views.forEach((view) => addEntities(entities, view));
|
||||
config.views.forEach((view) => {
|
||||
addEntities(entities, view);
|
||||
});
|
||||
return entities;
|
||||
};
|
||||
|
||||
|
@@ -94,12 +94,13 @@ export const handleAction = async (
|
||||
|
||||
switch (actionConfig.action) {
|
||||
case "more-info": {
|
||||
if (config.entity || config.camera_image || config.image_entity) {
|
||||
fireEvent(node, "hass-more-info", {
|
||||
entityId: (config.entity ||
|
||||
config.camera_image ||
|
||||
config.image_entity)!,
|
||||
});
|
||||
const entityId =
|
||||
actionConfig.entity_id ||
|
||||
config.entity ||
|
||||
config.camera_image ||
|
||||
config.image_entity;
|
||||
if (entityId) {
|
||||
fireEvent(node, "hass-more-info", { entityId });
|
||||
} else {
|
||||
showToast(node, {
|
||||
message: hass.localize(
|
||||
|
@@ -70,7 +70,7 @@ export class HuiHeadingCardEditor
|
||||
name: "heading_style",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
mode: "list",
|
||||
options: ["title", "subtitle"].map((value) => ({
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.heading.heading_style_options.${value}`
|
||||
|
@@ -36,7 +36,8 @@ export const DEFAULT_CONFIG: Partial<EntityHeadingBadgeConfig> = {
|
||||
|
||||
const entityConfigStruct = object({
|
||||
type: optional(string()),
|
||||
entity: string(),
|
||||
entity: optional(string()),
|
||||
name: optional(string()),
|
||||
icon: optional(string()),
|
||||
state_content: optional(union([string(), array(string())])),
|
||||
show_state: optional(boolean()),
|
||||
@@ -86,6 +87,12 @@ export class HuiHeadingEntityEditor
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "icon",
|
||||
selector: { icon: {} },
|
||||
@@ -128,7 +135,7 @@ export class HuiHeadingEntityEditor
|
||||
},
|
||||
{
|
||||
name: "state_content",
|
||||
selector: { ui_state_content: {} },
|
||||
selector: { ui_state_content: { allow_name: true } },
|
||||
context: { filter_entity: "entity" },
|
||||
},
|
||||
],
|
||||
@@ -213,7 +220,7 @@ export class HuiHeadingEntityEditor
|
||||
return;
|
||||
}
|
||||
|
||||
const config = ev.detail.value as FormData;
|
||||
const config = { ...ev.detail.value } as FormData;
|
||||
|
||||
if (config.displayed_elements) {
|
||||
config.show_state = config.displayed_elements.includes("state");
|
||||
@@ -269,6 +276,10 @@ export class HuiHeadingEntityEditor
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.heading.entity_config.${schema.name}_helper`
|
||||
);
|
||||
case "name":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.heading.entity_config.name_helper`
|
||||
);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
@@ -317,6 +317,7 @@ export abstract class HuiElementEditor<
|
||||
|
||||
private _handleUIConfigChanged(ev: UIConfigChangedEvent<T>) {
|
||||
ev.stopPropagation();
|
||||
if (!this.GUImode) return;
|
||||
const config = ev.detail.config;
|
||||
Object.keys(config).forEach((key) => {
|
||||
if (config[key] === undefined) {
|
||||
|
@@ -15,7 +15,6 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-circular-progress";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-icon-button";
|
||||
|
@@ -61,6 +61,11 @@ const actionConfigStructAssist = type({
|
||||
start_listening: optional(boolean()),
|
||||
});
|
||||
|
||||
const actionConfigStructMoreInfo = type({
|
||||
action: literal("more-info"),
|
||||
entity_id: optional(string()),
|
||||
});
|
||||
|
||||
export const actionConfigStructType = object({
|
||||
action: enums([
|
||||
"none",
|
||||
@@ -93,6 +98,9 @@ export const actionConfigStruct = dynamic<any>((value) => {
|
||||
case "assist": {
|
||||
return actionConfigStructAssist;
|
||||
}
|
||||
case "more-info": {
|
||||
return actionConfigStructMoreInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -507,12 +507,6 @@ export class HuiDialogEditView extends LitElement {
|
||||
margin-inline-end: auto;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
ha-circular-progress {
|
||||
display: none;
|
||||
}
|
||||
ha-circular-progress[indeterminate] {
|
||||
display: block;
|
||||
}
|
||||
.selected_menu_item {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
@@ -125,12 +125,15 @@ export class HuiEntityHeadingBadge
|
||||
"--icon-color": color,
|
||||
};
|
||||
|
||||
const name = config.name || stateObj.attributes.friendly_name;
|
||||
|
||||
return html`
|
||||
<ha-heading-badge
|
||||
.type=${hasAction(config.tap_action) ? "button" : "text"}
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler()}
|
||||
style=${styleMap(style)}
|
||||
.title=${name}
|
||||
>
|
||||
${config.show_icon
|
||||
? html`
|
||||
@@ -148,6 +151,8 @@ export class HuiEntityHeadingBadge
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.content=${config.state_content}
|
||||
.name=${config.name}
|
||||
dash-unavailable
|
||||
></state-display>
|
||||
`
|
||||
: nothing}
|
||||
|
@@ -16,6 +16,7 @@ export interface ErrorBadgeConfig extends LovelaceHeadingBadgeConfig {
|
||||
export interface EntityHeadingBadgeConfig extends LovelaceHeadingBadgeConfig {
|
||||
type?: "entity";
|
||||
entity: string;
|
||||
name?: string;
|
||||
state_content?: string | string[];
|
||||
icon?: string;
|
||||
show_state?: boolean;
|
||||
|
@@ -204,6 +204,10 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
grid-column: span min(var(--column-size, 1), var(--grid-column-count));
|
||||
}
|
||||
|
||||
.container.edit-mode .card {
|
||||
min-height: calc((var(--row-height) - var(--row-gap)) / 2);
|
||||
}
|
||||
|
||||
.card.fit-rows {
|
||||
height: calc(
|
||||
(var(--row-size, 1) * (var(--row-height) + var(--row-gap))) - var(
|
||||
|
@@ -57,6 +57,9 @@ class StateDisplay extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public name?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "dash-unavailable" })
|
||||
public dashUnavailable?: boolean;
|
||||
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
@@ -73,6 +76,9 @@ class StateDisplay extends LitElement {
|
||||
const domain = computeStateDomain(stateObj);
|
||||
|
||||
if (content === "state") {
|
||||
if (this.dashUnavailable && isUnavailableState(stateObj.state)) {
|
||||
return "—";
|
||||
}
|
||||
if (
|
||||
(stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP ||
|
||||
TIMESTAMP_STATE_DOMAINS.includes(domain)) &&
|
||||
@@ -93,31 +99,38 @@ class StateDisplay extends LitElement {
|
||||
if (content === "name") {
|
||||
return html`${this.name || stateObj.attributes.friendly_name}`;
|
||||
}
|
||||
|
||||
let relativeDateTime: string | undefined;
|
||||
|
||||
// Check last-changed for backwards compatibility
|
||||
if (content === "last_changed" || content === "last-changed") {
|
||||
return html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`;
|
||||
relativeDateTime = stateObj.last_changed;
|
||||
}
|
||||
// Check last_updated for backwards compatibility
|
||||
if (content === "last_updated" || content === "last-updated") {
|
||||
return html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.last_updated}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`;
|
||||
relativeDateTime = stateObj.last_updated;
|
||||
}
|
||||
if (content === "last_triggered") {
|
||||
|
||||
if (
|
||||
content === "last_triggered" ||
|
||||
(domain === "calendar" &&
|
||||
(content === "start_time" || content === "end_time")) ||
|
||||
(domain === "sun" &&
|
||||
(content === "next_dawn" ||
|
||||
content === "next_dusk" ||
|
||||
content === "next_midnight" ||
|
||||
content === "next_noon" ||
|
||||
content === "next_rising" ||
|
||||
content === "next_setting"))
|
||||
) {
|
||||
relativeDateTime = stateObj.attributes[content];
|
||||
}
|
||||
|
||||
if (relativeDateTime) {
|
||||
return html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.attributes.last_triggered}
|
||||
.datetime=${relativeDateTime}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`;
|
||||
|
@@ -1191,7 +1191,9 @@
|
||||
"skip": "Skip",
|
||||
"clear_skipped": "Clear skipped",
|
||||
"install": "Install",
|
||||
"create_backup": "Create backup before updating"
|
||||
"create_backup": "Create backup before updating",
|
||||
"auto_update_enabled_title": "Can not skip version",
|
||||
"auto_update_enabled_text": "Automatic updates for this item have been enabled; skipping it is, therefore, unavailable. You can either install this update now or wait for Home Assistant to do it automatically."
|
||||
},
|
||||
"updater": {
|
||||
"title": "Update instructions"
|
||||
@@ -4039,18 +4041,25 @@
|
||||
"unknown_automation": "Unknown automation",
|
||||
"create": "Create automation with {type}",
|
||||
"create_disable": "Can't create automation with disabled {type}",
|
||||
"new": {
|
||||
"title": "Create new automation",
|
||||
"description": "Start with an empty automation from scratch"
|
||||
},
|
||||
"triggers": {
|
||||
"caption": "Do something when…",
|
||||
"title": "Use device as trigger",
|
||||
"description": "When something happens to the device",
|
||||
"no_triggers": "No triggers",
|
||||
"unknown_trigger": "Unknown trigger"
|
||||
},
|
||||
"conditions": {
|
||||
"caption": "Only do something if…",
|
||||
"title": "Use device as condition",
|
||||
"description": "Only if a condition is met for the device",
|
||||
"no_conditions": "No conditions",
|
||||
"unknown_condition": "Unknown condition"
|
||||
},
|
||||
"actions": {
|
||||
"caption": "When something is triggered…",
|
||||
"title": "Use device as action",
|
||||
"description": "Do something on the device",
|
||||
"no_actions": "No actions",
|
||||
"unknown_action": "Unknown action"
|
||||
},
|
||||
@@ -4061,7 +4070,15 @@
|
||||
"scripts": "scripts",
|
||||
"no_scripts": "No scripts",
|
||||
"create": "Create script with {type}",
|
||||
"create_disable": "Can't create script with disabled {type}"
|
||||
"create_disable": "Can't create script with disabled {type}",
|
||||
"new": {
|
||||
"title": "Create new script",
|
||||
"description": "Start with an empty script from scratch"
|
||||
},
|
||||
"actions": {
|
||||
"title": "Use device as action",
|
||||
"description": "Do something on this device."
|
||||
}
|
||||
},
|
||||
"scene": {
|
||||
"scenes_heading": "Scenes",
|
||||
@@ -4089,8 +4106,10 @@
|
||||
},
|
||||
"confirm_rename_entity_ids": "Do you also want to rename the entity IDs of your entities?",
|
||||
"confirm_rename_entity_ids_warning": "This will not change any configuration (like automations, scripts, scenes, dashboards) that is currently using these entities! You will have to update them yourself to use the new entity IDs!",
|
||||
"confirm_rename_entity_will_rename": "The following entity IDs will be renamed",
|
||||
"confirm_rename_entity_wont_rename": "The following entity IDs will not be renamed as they do not contain the current device name ({deviceSlug})",
|
||||
"confirm_rename_entity_will_rename": "{count} {count, plural,\n one {entity ID}\n other {entity IDs}\n} will be renamed",
|
||||
"confirm_rename_new": "New",
|
||||
"confirm_rename_old": "Old",
|
||||
"confirm_rename_entity_wont_rename": "{count} {count, plural,\n one {entity ID}\n other {entity IDs}\n} will not be renamed as they do not contain the current device name ({deviceSlug})",
|
||||
"confirm_rename_entity_no_renamable_entity_ids": "No renamable entity IDs",
|
||||
"confirm_disable_config_entry": "There are no more devices for the config entry {entry_name}, do you want to instead disable the config entry?",
|
||||
"update_device_error": "Updating the device failed",
|
||||
@@ -4588,6 +4607,7 @@
|
||||
"confirm_delete_dataset": "Delete {name} dataset?",
|
||||
"confirm_delete_dataset_text": "This network will be removed from Home Assistant.",
|
||||
"no_border_routers": "No border routers found",
|
||||
"no_preferred_router": "No preferred border router defined",
|
||||
"border_routers": "{count} border {count, plural,\n one {router}\n other {routers}\n}",
|
||||
"managed_by_home_assistant": "Managed by Home Assistant",
|
||||
"operational_dataset": "Operational dataset",
|
||||
@@ -6026,6 +6046,8 @@
|
||||
"entity_config": {
|
||||
"color": "[%key:ui::panel::lovelace::editor::card::tile::color%]",
|
||||
"color_helper": "[%key:ui::panel::lovelace::editor::card::tile::color_helper%]",
|
||||
"name": "[%key:ui::panel::lovelace::editor::card::generic::name%]",
|
||||
"name_helper": "Visible if selected in state content",
|
||||
"visibility": "Visibility",
|
||||
"visibility_explanation": "The entity will be shown when ALL conditions below are fulfilled. If no conditions are set, the entity will always be shown.",
|
||||
"appearance": "Appearance",
|
||||
@@ -6877,7 +6899,8 @@
|
||||
"stop_listening": "Stop listening",
|
||||
"clear_events": "Clear events",
|
||||
"alert_event_type": "Event type is a mandatory field",
|
||||
"notification_event_fired": "Event {type} successfully fired!"
|
||||
"notification_event_fired": "Event {type} successfully fired!",
|
||||
"subscribe_failed": "Failed to subscribe to event: {error}"
|
||||
},
|
||||
"actions": {
|
||||
"title": "Actions",
|
||||
@@ -6962,8 +6985,18 @@
|
||||
"entity_no_longer_recorded": "This entity is no longer being recorded.",
|
||||
"no_state": "There is no state available for this entity."
|
||||
},
|
||||
"delete_selected": "Delete selected statistics",
|
||||
"multi_delete": {
|
||||
"title": "Delete selected statistics",
|
||||
"info_text": "Do you want to permanently delete the long term statistics {statistic_count, plural,\n one {of this entity}\n other {of {statistic_count} entities}\n} from your database?"
|
||||
},
|
||||
"fix_issue": {
|
||||
"fix": "Fix issue",
|
||||
"clearing_failed": "Clearing the statistics failed",
|
||||
"clearing_timeout_title": "Clearing not done yet",
|
||||
"clearing_timeout_text": "The clearing of the statistics took longer than expected, it might take longer for the issue to disappear.",
|
||||
"fix_all": "Fix all",
|
||||
"info": "Info",
|
||||
"no_support": {
|
||||
"title": "Fix issue",
|
||||
"info_text_1": "Fixing this issue is not supported yet."
|
||||
@@ -7022,6 +7055,7 @@
|
||||
},
|
||||
"adjust_sum": "Adjust sum",
|
||||
"data_table": {
|
||||
"select_all_issues": "Select all with issues",
|
||||
"name": "Name",
|
||||
"statistic_id": "Statistic id",
|
||||
"statistics_unit": "Statistics unit",
|
||||
|
@@ -42,8 +42,8 @@ describe("timerTimeRemaining", () => {
|
||||
state: "active",
|
||||
attributes: {
|
||||
remaining: "0:01:05",
|
||||
finishes_at: "2018-01-17T16:16:17+00:00",
|
||||
},
|
||||
last_changed: "2018-01-17T16:15:12Z",
|
||||
} as any),
|
||||
47
|
||||
);
|
||||
|
Reference in New Issue
Block a user