mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-25 00:59:29 +00:00
Compare commits
69 Commits
20220516.0
...
fix-button
Author | SHA1 | Date | |
---|---|---|---|
![]() |
026d027386 | ||
![]() |
d0ead1fdb8 | ||
![]() |
b0e6c41238 | ||
![]() |
2c1550b10f | ||
![]() |
ffc4ca5b56 | ||
![]() |
85ad6619b7 | ||
![]() |
7358faf88e | ||
![]() |
19d014307a | ||
![]() |
5217f5c50c | ||
![]() |
c4624faa71 | ||
![]() |
b35ba4d673 | ||
![]() |
f8303bff76 | ||
![]() |
e61aa266a6 | ||
![]() |
d7971c69ad | ||
![]() |
966a624ef6 | ||
![]() |
7cc576a616 | ||
![]() |
2dec8e70ec | ||
![]() |
97663aef42 | ||
![]() |
3f1a2526b3 | ||
![]() |
e7517a8b61 | ||
![]() |
e3d394eb32 | ||
![]() |
536ea822b3 | ||
![]() |
8e4e22b6f8 | ||
![]() |
2eaa246a03 | ||
![]() |
e841bf89be | ||
![]() |
36e1203fb1 | ||
![]() |
3acab5a39c | ||
![]() |
49cfde1fe7 | ||
![]() |
49c018c000 | ||
![]() |
b71b230bfd | ||
![]() |
e1fd7244a5 | ||
![]() |
067c2fdfa8 | ||
![]() |
a02b817d7f | ||
![]() |
7db6e0b779 | ||
![]() |
1d5cc91a2d | ||
![]() |
0623e7dce4 | ||
![]() |
da106d278c | ||
![]() |
51c5ab33f0 | ||
![]() |
8ac4a6d900 | ||
![]() |
fae1bcf0e0 | ||
![]() |
9a9eec40b2 | ||
![]() |
6ab19d66d5 | ||
![]() |
a0a7ce014f | ||
![]() |
bfeb90780f | ||
![]() |
1f105b6c15 | ||
![]() |
5b7b0ea326 | ||
![]() |
32a991989f | ||
![]() |
788f76ab9c | ||
![]() |
f6411dce66 | ||
![]() |
6f19ea1d84 | ||
![]() |
448609533f | ||
![]() |
6c48ace41e | ||
![]() |
c41e100c1c | ||
![]() |
8216b522c2 | ||
![]() |
82035d587a | ||
![]() |
2796c3570a | ||
![]() |
f4f51e1de5 | ||
![]() |
af6b0d3266 | ||
![]() |
7d1c77a38f | ||
![]() |
f807618f75 | ||
![]() |
4cfb6713cb | ||
![]() |
d32f84f28d | ||
![]() |
5fb1504211 | ||
![]() |
c37e1f0c9d | ||
![]() |
90c234ffad | ||
![]() |
dd3a3ec586 | ||
![]() |
6f67da09c0 | ||
![]() |
ba27c184f6 | ||
![]() |
b37f97128a |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -51,7 +51,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
|||||||
<!--
|
<!--
|
||||||
Provide details about the versions you are using, which helps us reproducing
|
Provide details about the versions you are using, which helps us reproducing
|
||||||
and finding the issue quicker. Version information is found in the
|
and finding the issue quicker. Version information is found in the
|
||||||
Home Assistant frontend: Configuration -> Info.
|
Home Assistant frontend: Settings -> About.
|
||||||
|
|
||||||
Browser version and operating system is important! Please try to replicate
|
Browser version and operating system is important! Please try to replicate
|
||||||
your issue in a different browser and be sure to include your findings.
|
your issue in a different browser and be sure to include your findings.
|
||||||
|
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -64,7 +64,7 @@ body:
|
|||||||
label: What version of Home Assistant Core has the issue?
|
label: What version of Home Assistant Core has the issue?
|
||||||
placeholder: core-
|
placeholder: core-
|
||||||
description: >
|
description: >
|
||||||
Can be found in the Configuration panel -> Info.
|
Can be found in: [Settings -> About](https://my.home-assistant.io/redirect/info/).
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: What was the last working version of Home Assistant Core?
|
label: What was the last working version of Home Assistant Core?
|
||||||
|
@@ -26,8 +26,8 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
version() {
|
version() {
|
||||||
const version = fs
|
const version = fs
|
||||||
.readFileSync(path.resolve(paths.polymer_dir, "setup.cfg"), "utf8")
|
.readFileSync(path.resolve(paths.polymer_dir, "pyproject.toml"), "utf8")
|
||||||
.match(/version\W+=\W(\d{8}\.\d)/);
|
.match(/version\W+=\W"(\d{8}\.\d)"/);
|
||||||
if (!version) {
|
if (!version) {
|
||||||
throw Error("Version not found");
|
throw Error("Version not found");
|
||||||
}
|
}
|
||||||
|
@@ -194,7 +194,7 @@ export const demoLovelaceJimpower: DemoConfig["lovelace"] = () => ({
|
|||||||
type: "state-icon",
|
type: "state-icon",
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "call-service",
|
action: "call-service",
|
||||||
service_data: {
|
data: {
|
||||||
entity_id: "group.downstairs_lights",
|
entity_id: "group.downstairs_lights",
|
||||||
},
|
},
|
||||||
service: "homeassistant.toggle",
|
service: "homeassistant.toggle",
|
||||||
|
@@ -377,7 +377,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
|||||||
name: "AC bed",
|
name: "AC bed",
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "call-service",
|
action: "call-service",
|
||||||
service_data: {
|
data: {
|
||||||
entity_id: "script.air_cleaner_quiet",
|
entity_id: "script.air_cleaner_quiet",
|
||||||
},
|
},
|
||||||
service: "script.turn_on",
|
service: "script.turn_on",
|
||||||
@@ -390,7 +390,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
|||||||
name: "AC bed",
|
name: "AC bed",
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "call-service",
|
action: "call-service",
|
||||||
service_data: {
|
data: {
|
||||||
entity_id: "script.air_cleaner_auto",
|
entity_id: "script.air_cleaner_auto",
|
||||||
},
|
},
|
||||||
service: "script.turn_on",
|
service: "script.turn_on",
|
||||||
@@ -403,7 +403,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
|||||||
name: "AC bed",
|
name: "AC bed",
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "call-service",
|
action: "call-service",
|
||||||
service_data: {
|
data: {
|
||||||
entity_id: "script.air_cleaner_turbo",
|
entity_id: "script.air_cleaner_turbo",
|
||||||
},
|
},
|
||||||
service: "script.turn_on",
|
service: "script.turn_on",
|
||||||
@@ -416,7 +416,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
|||||||
name: "AC",
|
name: "AC",
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "call-service",
|
action: "call-service",
|
||||||
service_data: {
|
data: {
|
||||||
entity_id: "script.ac_off",
|
entity_id: "script.ac_off",
|
||||||
},
|
},
|
||||||
service: "script.turn_on",
|
service: "script.turn_on",
|
||||||
@@ -429,7 +429,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
|||||||
name: "AC",
|
name: "AC",
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "call-service",
|
action: "call-service",
|
||||||
service_data: {
|
data: {
|
||||||
entity_id: "script.ac_on",
|
entity_id: "script.ac_on",
|
||||||
},
|
},
|
||||||
service: "script.turn_on",
|
service: "script.turn_on",
|
||||||
@@ -629,7 +629,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
|||||||
entity: "scene.morning_lights",
|
entity: "scene.morning_lights",
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "call-service",
|
action: "call-service",
|
||||||
service_data: {
|
data: {
|
||||||
entity_id: "scene.morning_lights",
|
entity_id: "scene.morning_lights",
|
||||||
},
|
},
|
||||||
service: "scene.turn_on",
|
service: "scene.turn_on",
|
||||||
@@ -641,7 +641,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
|||||||
entity: "scene.movie_time",
|
entity: "scene.movie_time",
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "call-service",
|
action: "call-service",
|
||||||
service_data: {
|
data: {
|
||||||
entity_id: "scene.movie_time",
|
entity_id: "scene.movie_time",
|
||||||
},
|
},
|
||||||
service: "scene.turn_on",
|
service: "scene.turn_on",
|
||||||
@@ -702,7 +702,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
|||||||
entity: "light.downstairs_lights",
|
entity: "light.downstairs_lights",
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "call-service",
|
action: "call-service",
|
||||||
service_data: {
|
data: {
|
||||||
entity_id: "light.downstairs_lights",
|
entity_id: "light.downstairs_lights",
|
||||||
},
|
},
|
||||||
service: "light.toggle",
|
service: "light.toggle",
|
||||||
@@ -714,7 +714,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
|||||||
entity: "light.upstairs_lights",
|
entity: "light.upstairs_lights",
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "call-service",
|
action: "call-service",
|
||||||
service_data: {
|
data: {
|
||||||
entity_id: "light.upstairs_lights",
|
entity_id: "light.upstairs_lights",
|
||||||
},
|
},
|
||||||
service: "light.toggle",
|
service: "light.toggle",
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { format, startOfToday, startOfTomorrow } from "date-fns";
|
import { format, startOfToday, startOfTomorrow } from "date-fns/esm";
|
||||||
import { EnergySolarForecasts } from "../../../src/data/energy";
|
import { EnergySolarForecasts } from "../../../src/data/energy";
|
||||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||||
|
|
||||||
|
@@ -4,7 +4,7 @@ import {
|
|||||||
addMonths,
|
addMonths,
|
||||||
differenceInHours,
|
differenceInHours,
|
||||||
endOfDay,
|
endOfDay,
|
||||||
} from "date-fns";
|
} from "date-fns/esm";
|
||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { StatisticValue } from "../../../src/data/history";
|
import { StatisticValue } from "../../../src/data/history";
|
||||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||||
|
@@ -119,7 +119,7 @@ export const basicTrace: DemoTrace = {
|
|||||||
params: {
|
params: {
|
||||||
domain: "input_boolean",
|
domain: "input_boolean",
|
||||||
service: "toggle",
|
service: "toggle",
|
||||||
service_data: {},
|
data: {},
|
||||||
target: {
|
target: {
|
||||||
entity_id: ["input_boolean.toggle_4"],
|
entity_id: ["input_boolean.toggle_4"],
|
||||||
},
|
},
|
||||||
@@ -164,7 +164,7 @@ export const basicTrace: DemoTrace = {
|
|||||||
params: {
|
params: {
|
||||||
domain: "input_boolean",
|
domain: "input_boolean",
|
||||||
service: "toggle",
|
service: "toggle",
|
||||||
service_data: {},
|
data: {},
|
||||||
target: {
|
target: {
|
||||||
entity_id: ["input_boolean.toggle_2"],
|
entity_id: ["input_boolean.toggle_2"],
|
||||||
},
|
},
|
||||||
@@ -182,7 +182,7 @@ export const basicTrace: DemoTrace = {
|
|||||||
params: {
|
params: {
|
||||||
domain: "input_boolean",
|
domain: "input_boolean",
|
||||||
service: "toggle",
|
service: "toggle",
|
||||||
service_data: {},
|
data: {},
|
||||||
target: {
|
target: {
|
||||||
entity_id: ["input_boolean.toggle_3"],
|
entity_id: ["input_boolean.toggle_3"],
|
||||||
},
|
},
|
||||||
@@ -200,7 +200,7 @@ export const basicTrace: DemoTrace = {
|
|||||||
params: {
|
params: {
|
||||||
domain: "input_boolean",
|
domain: "input_boolean",
|
||||||
service: "toggle",
|
service: "toggle",
|
||||||
service_data: {},
|
data: {},
|
||||||
target: {
|
target: {
|
||||||
entity_id: ["input_boolean.toggle_4"],
|
entity_id: ["input_boolean.toggle_4"],
|
||||||
},
|
},
|
||||||
|
@@ -249,7 +249,7 @@ const CONFIGS = [
|
|||||||
name: Bed light
|
name: Bed light
|
||||||
action_name: Toggle light
|
action_name: Toggle light
|
||||||
service: light.toggle
|
service: light.toggle
|
||||||
service_data:
|
data:
|
||||||
entity_id: light.bed_light
|
entity_id: light.bed_light
|
||||||
- type: section
|
- type: section
|
||||||
label: Links
|
label: Links
|
||||||
|
@@ -199,7 +199,7 @@ const CONFIGS = [
|
|||||||
tap_action:
|
tap_action:
|
||||||
action: call-service
|
action: call-service
|
||||||
service: light.turn_on
|
service: light.turn_on
|
||||||
service_data:
|
data:
|
||||||
entity_id: light.ceiling_lights
|
entity_id: light.ceiling_lights
|
||||||
- entity: sun.sun
|
- entity: sun.sun
|
||||||
name: Regular
|
name: Regular
|
||||||
|
@@ -40,7 +40,7 @@ const CONFIGS = [
|
|||||||
left: 90%
|
left: 90%
|
||||||
padding: 0px
|
padding: 0px
|
||||||
service: light.turn_off
|
service: light.turn_off
|
||||||
service_data:
|
data:
|
||||||
entity_id: group.all_lights
|
entity_id: group.all_lights
|
||||||
- type: icon
|
- type: icon
|
||||||
icon: mdi:cctv
|
icon: mdi:cctv
|
||||||
@@ -88,7 +88,7 @@ const CONFIGS = [
|
|||||||
left: 90%
|
left: 90%
|
||||||
padding: 0px
|
padding: 0px
|
||||||
service: light.turn_off
|
service: light.turn_off
|
||||||
service_data:
|
data:
|
||||||
entity_id: group.all_lights
|
entity_id: group.all_lights
|
||||||
- type: icon
|
- type: icon
|
||||||
icon: mdi:cctv
|
icon: mdi:cctv
|
||||||
|
@@ -17,7 +17,10 @@ import {
|
|||||||
HassioAddonDetails,
|
HassioAddonDetails,
|
||||||
} from "../../../src/data/hassio/addon";
|
} from "../../../src/data/hassio/addon";
|
||||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||||
import { setSupervisorOption } from "../../../src/data/hassio/supervisor";
|
import {
|
||||||
|
fetchHassioSupervisorInfo,
|
||||||
|
setSupervisorOption,
|
||||||
|
} from "../../../src/data/hassio/supervisor";
|
||||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||||
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||||
import "../../../src/layouts/hass-error-screen";
|
import "../../../src/layouts/hass-error-screen";
|
||||||
@@ -169,38 +172,40 @@ class HassioAddonDashboard extends LitElement {
|
|||||||
if (this.route.path === "") {
|
if (this.route.path === "") {
|
||||||
const requestedAddon = extractSearchParam("addon");
|
const requestedAddon = extractSearchParam("addon");
|
||||||
const requestedAddonRepository = extractSearchParam("repository_url");
|
const requestedAddonRepository = extractSearchParam("repository_url");
|
||||||
if (
|
if (requestedAddonRepository) {
|
||||||
requestedAddonRepository &&
|
const supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
|
||||||
!this.supervisor.supervisor.addons_repositories.find(
|
|
||||||
(repo) => repo === requestedAddonRepository
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
if (
|
if (
|
||||||
!(await showConfirmationDialog(this, {
|
!supervisorInfo.addons_repositories.find(
|
||||||
title: this.supervisor.localize("my.add_addon_repository_title"),
|
(repo) => repo === requestedAddonRepository
|
||||||
text: this.supervisor.localize(
|
)
|
||||||
"my.add_addon_repository_description",
|
|
||||||
{ addon: requestedAddon, repository: requestedAddonRepository }
|
|
||||||
),
|
|
||||||
confirmText: this.supervisor.localize("common.add"),
|
|
||||||
dismissText: this.supervisor.localize("common.cancel"),
|
|
||||||
}))
|
|
||||||
) {
|
) {
|
||||||
this._error = this.supervisor.localize(
|
if (
|
||||||
"my.error_repository_not_found"
|
!(await showConfirmationDialog(this, {
|
||||||
);
|
title: this.supervisor.localize("my.add_addon_repository_title"),
|
||||||
return;
|
text: this.supervisor.localize(
|
||||||
}
|
"my.add_addon_repository_description",
|
||||||
|
{ addon: requestedAddon, repository: requestedAddonRepository }
|
||||||
|
),
|
||||||
|
confirmText: this.supervisor.localize("common.add"),
|
||||||
|
dismissText: this.supervisor.localize("common.cancel"),
|
||||||
|
}))
|
||||||
|
) {
|
||||||
|
this._error = this.supervisor.localize(
|
||||||
|
"my.error_repository_not_found"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await setSupervisorOption(this.hass, {
|
await setSupervisorOption(this.hass, {
|
||||||
addons_repositories: [
|
addons_repositories: [
|
||||||
...this.supervisor.supervisor.addons_repositories,
|
...supervisorInfo.addons_repositories,
|
||||||
requestedAddonRepository,
|
requestedAddonRepository,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this._error = extractApiErrorMessage(err);
|
this._error = extractApiErrorMessage(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -72,8 +72,8 @@
|
|||||||
"@material/mwc-textfield": "0.25.3",
|
"@material/mwc-textfield": "0.25.3",
|
||||||
"@material/mwc-top-app-bar-fixed": "^0.25.3",
|
"@material/mwc-top-app-bar-fixed": "^0.25.3",
|
||||||
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
|
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
|
||||||
"@mdi/js": "6.6.95",
|
"@mdi/js": "6.7.96",
|
||||||
"@mdi/svg": "6.6.95",
|
"@mdi/svg": "6.7.96",
|
||||||
"@polymer/app-layout": "^3.1.0",
|
"@polymer/app-layout": "^3.1.0",
|
||||||
"@polymer/iron-flex-layout": "^3.0.1",
|
"@polymer/iron-flex-layout": "^3.0.1",
|
||||||
"@polymer/iron-icon": "^3.0.1",
|
"@polymer/iron-icon": "^3.0.1",
|
||||||
|
@@ -1,3 +1,30 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools~=60.5", "wheel~=0.37.1"]
|
requires = ["setuptools~=62.3", "wheel~=0.37.1"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "home-assistant-frontend"
|
||||||
|
version = "20220525.0"
|
||||||
|
license = {text = "Apache-2.0"}
|
||||||
|
description = "The Home Assistant frontend"
|
||||||
|
readme = "README.md"
|
||||||
|
authors = [
|
||||||
|
{name = "The Home Assistant Authors", email = "hello@home-assistant.io"}
|
||||||
|
]
|
||||||
|
requires-python = ">=3.4.0"
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
"Homepage" = "https://github.com/home-assistant/frontend"
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
platforms = ["any"]
|
||||||
|
zip-safe = false
|
||||||
|
include-package-data = true
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
include = ["hass_frontend*"]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
python_version = 3.4
|
||||||
|
show_error_codes = true
|
||||||
|
strict = true
|
||||||
|
@@ -50,14 +50,14 @@ async function main(args) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setup = fs.readFileSync("setup.cfg", "utf8");
|
const setup = fs.readFileSync("pyproject.toml", "utf8");
|
||||||
const version = setup.match(/\d{8}\.\d+/)[0];
|
const version = setup.match(/version\W+=\W"(\d{8}\.\d)"/)[1];
|
||||||
const newVersion = method(version);
|
const newVersion = method(version);
|
||||||
|
|
||||||
console.log("Current version:", version);
|
console.log("Current version:", version);
|
||||||
console.log("New version:", newVersion);
|
console.log("New version:", newVersion);
|
||||||
|
|
||||||
fs.writeFileSync("setup.cfg", setup.replace(version, newVersion), "utf-8");
|
fs.writeFileSync("pyproject.toml", setup.replace(version, newVersion), "utf-8");
|
||||||
|
|
||||||
if (!commit) {
|
if (!commit) {
|
||||||
return;
|
return;
|
||||||
|
26
setup.cfg
26
setup.cfg
@@ -1,26 +0,0 @@
|
|||||||
[metadata]
|
|
||||||
name = home-assistant-frontend
|
|
||||||
version = 20220516.0
|
|
||||||
author = The Home Assistant Authors
|
|
||||||
author_email = hello@home-assistant.io
|
|
||||||
license = Apache-2.0
|
|
||||||
platforms = any
|
|
||||||
description = The Home Assistant frontend
|
|
||||||
long_description = file: README.md
|
|
||||||
long_description_content_type = text/markdown
|
|
||||||
url = https://github.com/home-assistant/frontend
|
|
||||||
|
|
||||||
[options]
|
|
||||||
packages = find:
|
|
||||||
zip_safe = False
|
|
||||||
include_package_data = True
|
|
||||||
python_requires = >= 3.4.0
|
|
||||||
|
|
||||||
[options.packages.find]
|
|
||||||
include =
|
|
||||||
hass_frontend*
|
|
||||||
|
|
||||||
[mypy]
|
|
||||||
python_version = 3.4
|
|
||||||
show_error_codes = True
|
|
||||||
strict = True
|
|
@@ -1,6 +1,11 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import { UNAVAILABLE_STATES } from "../../data/entity";
|
||||||
|
|
||||||
export const computeActiveState = (stateObj: HassEntity): string => {
|
export const computeActiveState = (stateObj: HassEntity): string => {
|
||||||
|
if (UNAVAILABLE_STATES.includes(stateObj.state)) {
|
||||||
|
return stateObj.state;
|
||||||
|
}
|
||||||
|
|
||||||
const domain = stateObj.entity_id.split(".")[0];
|
const domain = stateObj.entity_id.split(".")[0];
|
||||||
let state = stateObj.state;
|
let state = stateObj.state;
|
||||||
|
|
||||||
|
@@ -2,67 +2,74 @@ import { HassEntity } from "home-assistant-js-websocket";
|
|||||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||||
import { FrontendLocaleData } from "../../data/translation";
|
import { FrontendLocaleData } from "../../data/translation";
|
||||||
import {
|
import {
|
||||||
updateIsInstalling,
|
|
||||||
UpdateEntity,
|
|
||||||
UPDATE_SUPPORT_PROGRESS,
|
UPDATE_SUPPORT_PROGRESS,
|
||||||
|
updateIsInstallingFromAttributes,
|
||||||
} from "../../data/update";
|
} from "../../data/update";
|
||||||
import { formatDate } from "../datetime/format_date";
|
import { formatDate } from "../datetime/format_date";
|
||||||
import { formatDateTime } from "../datetime/format_date_time";
|
import { formatDateTime } from "../datetime/format_date_time";
|
||||||
import { formatTime } from "../datetime/format_time";
|
import { formatTime } from "../datetime/format_time";
|
||||||
import { formatNumber, isNumericState } from "../number/format_number";
|
import { formatNumber, isNumericFromAttributes } from "../number/format_number";
|
||||||
import { LocalizeFunc } from "../translations/localize";
|
import { LocalizeFunc } from "../translations/localize";
|
||||||
import { computeStateDomain } from "./compute_state_domain";
|
import { supportsFeatureFromAttributes } from "./supports-feature";
|
||||||
import { supportsFeature } from "./supports-feature";
|
|
||||||
import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration";
|
import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration";
|
||||||
|
import { computeDomain } from "./compute_domain";
|
||||||
|
|
||||||
export const computeStateDisplay = (
|
export const computeStateDisplay = (
|
||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
stateObj: HassEntity,
|
stateObj: HassEntity,
|
||||||
locale: FrontendLocaleData,
|
locale: FrontendLocaleData,
|
||||||
state?: string
|
state?: string
|
||||||
): string => {
|
): string =>
|
||||||
const compareState = state !== undefined ? state : stateObj.state;
|
computeStateDisplayFromEntityAttributes(
|
||||||
|
localize,
|
||||||
|
locale,
|
||||||
|
stateObj.entity_id,
|
||||||
|
stateObj.attributes,
|
||||||
|
state !== undefined ? state : stateObj.state
|
||||||
|
);
|
||||||
|
|
||||||
if (compareState === UNKNOWN || compareState === UNAVAILABLE) {
|
export const computeStateDisplayFromEntityAttributes = (
|
||||||
return localize(`state.default.${compareState}`);
|
localize: LocalizeFunc,
|
||||||
|
locale: FrontendLocaleData,
|
||||||
|
entityId: string,
|
||||||
|
attributes: any,
|
||||||
|
state: string
|
||||||
|
): string => {
|
||||||
|
if (state === UNKNOWN || state === UNAVAILABLE) {
|
||||||
|
return localize(`state.default.${state}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
|
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
|
||||||
if (isNumericState(stateObj)) {
|
if (isNumericFromAttributes(attributes)) {
|
||||||
// state is duration
|
// state is duration
|
||||||
if (
|
if (
|
||||||
stateObj.attributes.device_class === "duration" &&
|
attributes.device_class === "duration" &&
|
||||||
stateObj.attributes.unit_of_measurement &&
|
attributes.unit_of_measurement &&
|
||||||
UNIT_TO_SECOND_CONVERT[stateObj.attributes.unit_of_measurement]
|
UNIT_TO_SECOND_CONVERT[attributes.unit_of_measurement]
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
return formatDuration(
|
return formatDuration(state, attributes.unit_of_measurement);
|
||||||
compareState,
|
|
||||||
stateObj.attributes.unit_of_measurement
|
|
||||||
);
|
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
// fallback to default
|
// fallback to default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (stateObj.attributes.device_class === "monetary") {
|
if (attributes.device_class === "monetary") {
|
||||||
try {
|
try {
|
||||||
return formatNumber(compareState, locale, {
|
return formatNumber(state, locale, {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: stateObj.attributes.unit_of_measurement,
|
currency: attributes.unit_of_measurement,
|
||||||
minimumFractionDigits: 2,
|
minimumFractionDigits: 2,
|
||||||
});
|
});
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
// fallback to default
|
// fallback to default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return `${formatNumber(compareState, locale)}${
|
return `${formatNumber(state, locale)}${
|
||||||
stateObj.attributes.unit_of_measurement
|
attributes.unit_of_measurement ? " " + attributes.unit_of_measurement : ""
|
||||||
? " " + stateObj.attributes.unit_of_measurement
|
|
||||||
: ""
|
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const domain = computeStateDomain(stateObj);
|
const domain = computeDomain(entityId);
|
||||||
|
|
||||||
if (domain === "input_datetime") {
|
if (domain === "input_datetime") {
|
||||||
if (state !== undefined) {
|
if (state !== undefined) {
|
||||||
@@ -97,36 +104,32 @@ export const computeStateDisplay = (
|
|||||||
} else {
|
} else {
|
||||||
// If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format.
|
// If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format.
|
||||||
let date: Date;
|
let date: Date;
|
||||||
if (stateObj.attributes.has_date && stateObj.attributes.has_time) {
|
if (attributes.has_date && attributes.has_time) {
|
||||||
date = new Date(
|
date = new Date(
|
||||||
stateObj.attributes.year,
|
attributes.year,
|
||||||
stateObj.attributes.month - 1,
|
attributes.month - 1,
|
||||||
stateObj.attributes.day,
|
attributes.day,
|
||||||
stateObj.attributes.hour,
|
attributes.hour,
|
||||||
stateObj.attributes.minute
|
attributes.minute
|
||||||
);
|
);
|
||||||
return formatDateTime(date, locale);
|
return formatDateTime(date, locale);
|
||||||
}
|
}
|
||||||
if (stateObj.attributes.has_date) {
|
if (attributes.has_date) {
|
||||||
date = new Date(
|
date = new Date(attributes.year, attributes.month - 1, attributes.day);
|
||||||
stateObj.attributes.year,
|
|
||||||
stateObj.attributes.month - 1,
|
|
||||||
stateObj.attributes.day
|
|
||||||
);
|
|
||||||
return formatDate(date, locale);
|
return formatDate(date, locale);
|
||||||
}
|
}
|
||||||
if (stateObj.attributes.has_time) {
|
if (attributes.has_time) {
|
||||||
date = new Date();
|
date = new Date();
|
||||||
date.setHours(stateObj.attributes.hour, stateObj.attributes.minute);
|
date.setHours(attributes.hour, attributes.minute);
|
||||||
return formatTime(date, locale);
|
return formatTime(date, locale);
|
||||||
}
|
}
|
||||||
return stateObj.state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (domain === "humidifier") {
|
if (domain === "humidifier") {
|
||||||
if (compareState === "on" && stateObj.attributes.humidity) {
|
if (state === "on" && attributes.humidity) {
|
||||||
return `${stateObj.attributes.humidity} %`;
|
return `${attributes.humidity} %`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +139,7 @@ export const computeStateDisplay = (
|
|||||||
domain === "number" ||
|
domain === "number" ||
|
||||||
domain === "input_number"
|
domain === "input_number"
|
||||||
) {
|
) {
|
||||||
return formatNumber(compareState, locale);
|
return formatNumber(state, locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
// state of button is a timestamp
|
// state of button is a timestamp
|
||||||
@@ -144,12 +147,12 @@ export const computeStateDisplay = (
|
|||||||
domain === "button" ||
|
domain === "button" ||
|
||||||
domain === "input_button" ||
|
domain === "input_button" ||
|
||||||
domain === "scene" ||
|
domain === "scene" ||
|
||||||
(domain === "sensor" && stateObj.attributes.device_class === "timestamp")
|
(domain === "sensor" && attributes.device_class === "timestamp")
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
return formatDateTime(new Date(compareState), locale);
|
return formatDateTime(new Date(state), locale);
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
return compareState;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,30 +163,28 @@ export const computeStateDisplay = (
|
|||||||
// When the latest version is skipped, show the latest version
|
// When the latest version is skipped, show the latest version
|
||||||
// When update is not available, show "Up-to-date"
|
// When update is not available, show "Up-to-date"
|
||||||
// When update is not available and there is no latest_version show "Unavailable"
|
// When update is not available and there is no latest_version show "Unavailable"
|
||||||
return compareState === "on"
|
return state === "on"
|
||||||
? updateIsInstalling(stateObj as UpdateEntity)
|
? updateIsInstallingFromAttributes(attributes)
|
||||||
? supportsFeature(stateObj, UPDATE_SUPPORT_PROGRESS)
|
? supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS)
|
||||||
? localize("ui.card.update.installing_with_progress", {
|
? localize("ui.card.update.installing_with_progress", {
|
||||||
progress: stateObj.attributes.in_progress,
|
progress: attributes.in_progress,
|
||||||
})
|
})
|
||||||
: localize("ui.card.update.installing")
|
: localize("ui.card.update.installing")
|
||||||
: stateObj.attributes.latest_version
|
: attributes.latest_version
|
||||||
: stateObj.attributes.skipped_version ===
|
: attributes.skipped_version === attributes.latest_version
|
||||||
stateObj.attributes.latest_version
|
? attributes.latest_version ?? localize("state.default.unavailable")
|
||||||
? stateObj.attributes.latest_version ??
|
|
||||||
localize("state.default.unavailable")
|
|
||||||
: localize("ui.card.update.up_to_date");
|
: localize("ui.card.update.up_to_date");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Return device class translation
|
// Return device class translation
|
||||||
(stateObj.attributes.device_class &&
|
(attributes.device_class &&
|
||||||
localize(
|
localize(
|
||||||
`component.${domain}.state.${stateObj.attributes.device_class}.${compareState}`
|
`component.${domain}.state.${attributes.device_class}.${state}`
|
||||||
)) ||
|
)) ||
|
||||||
// Return default translation
|
// Return default translation
|
||||||
localize(`component.${domain}.state._.${compareState}`) ||
|
localize(`component.${domain}.state._.${state}`) ||
|
||||||
// We don't know! Return the raw state.
|
// We don't know! Return the raw state.
|
||||||
compareState
|
state
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,7 +1,13 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { computeObjectId } from "./compute_object_id";
|
import { computeObjectId } from "./compute_object_id";
|
||||||
|
|
||||||
|
export const computeStateNameFromEntityAttributes = (
|
||||||
|
entityId: string,
|
||||||
|
attributes: { [key: string]: any }
|
||||||
|
): string =>
|
||||||
|
attributes.friendly_name === undefined
|
||||||
|
? computeObjectId(entityId).replace(/_/g, " ")
|
||||||
|
: attributes.friendly_name || "";
|
||||||
|
|
||||||
export const computeStateName = (stateObj: HassEntity): string =>
|
export const computeStateName = (stateObj: HassEntity): string =>
|
||||||
stateObj.attributes.friendly_name === undefined
|
computeStateNameFromEntityAttributes(stateObj.entity_id, stateObj.attributes);
|
||||||
? computeObjectId(stateObj.entity_id).replace(/_/g, " ")
|
|
||||||
: stateObj.attributes.friendly_name || "";
|
|
||||||
|
@@ -29,7 +29,8 @@ import {
|
|||||||
mdiWeatherNight,
|
mdiWeatherNight,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { updateIsInstalling, UpdateEntity } from "../../data/update";
|
import { UpdateEntity, updateIsInstalling } from "../../data/update";
|
||||||
|
import { weatherIcon } from "../../data/weather";
|
||||||
/**
|
/**
|
||||||
* Return the icon to be used for a domain.
|
* Return the icon to be used for a domain.
|
||||||
*
|
*
|
||||||
@@ -46,6 +47,20 @@ export const domainIcon = (
|
|||||||
stateObj?: HassEntity,
|
stateObj?: HassEntity,
|
||||||
state?: string
|
state?: string
|
||||||
): string => {
|
): string => {
|
||||||
|
const icon = domainIconWithoutDefault(domain, stateObj, state);
|
||||||
|
if (icon) {
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.warn(`Unable to find icon for domain ${domain}`);
|
||||||
|
return DEFAULT_DOMAIN_ICON;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const domainIconWithoutDefault = (
|
||||||
|
domain: string,
|
||||||
|
stateObj?: HassEntity,
|
||||||
|
state?: string
|
||||||
|
): string | undefined => {
|
||||||
const compareState = state !== undefined ? state : stateObj?.state;
|
const compareState = state !== undefined ? state : stateObj?.state;
|
||||||
|
|
||||||
switch (domain) {
|
switch (domain) {
|
||||||
@@ -87,6 +102,15 @@ export const domainIcon = (
|
|||||||
? mdiCheckCircleOutline
|
? mdiCheckCircleOutline
|
||||||
: mdiCloseCircleOutline;
|
: mdiCloseCircleOutline;
|
||||||
|
|
||||||
|
case "input_datetime":
|
||||||
|
if (!stateObj?.attributes.has_date) {
|
||||||
|
return mdiClock;
|
||||||
|
}
|
||||||
|
if (!stateObj.attributes.has_time) {
|
||||||
|
return mdiCalendar;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case "lock":
|
case "lock":
|
||||||
switch (compareState) {
|
switch (compareState) {
|
||||||
case "unlocked":
|
case "unlocked":
|
||||||
@@ -124,15 +148,6 @@ export const domainIcon = (
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "input_datetime":
|
|
||||||
if (!stateObj?.attributes.has_date) {
|
|
||||||
return mdiClock;
|
|
||||||
}
|
|
||||||
if (!stateObj.attributes.has_time) {
|
|
||||||
return mdiCalendar;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "sun":
|
case "sun":
|
||||||
return stateObj?.state === "above_horizon"
|
return stateObj?.state === "above_horizon"
|
||||||
? FIXED_DOMAIN_ICONS[domain]
|
? FIXED_DOMAIN_ICONS[domain]
|
||||||
@@ -144,13 +159,14 @@ export const domainIcon = (
|
|||||||
? mdiPackageDown
|
? mdiPackageDown
|
||||||
: mdiPackageUp
|
: mdiPackageUp
|
||||||
: mdiPackage;
|
: mdiPackage;
|
||||||
|
|
||||||
|
case "weather":
|
||||||
|
return weatherIcon(stateObj?.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (domain in FIXED_DOMAIN_ICONS) {
|
if (domain in FIXED_DOMAIN_ICONS) {
|
||||||
return FIXED_DOMAIN_ICONS[domain];
|
return FIXED_DOMAIN_ICONS[domain];
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line
|
return undefined;
|
||||||
console.warn(`Unable to find icon for domain ${domain}`);
|
|
||||||
return DEFAULT_DOMAIN_ICON;
|
|
||||||
};
|
};
|
||||||
|
@@ -3,6 +3,13 @@ import { HassEntity } from "home-assistant-js-websocket";
|
|||||||
export const supportsFeature = (
|
export const supportsFeature = (
|
||||||
stateObj: HassEntity,
|
stateObj: HassEntity,
|
||||||
feature: number
|
feature: number
|
||||||
|
): boolean => supportsFeatureFromAttributes(stateObj.attributes, feature);
|
||||||
|
|
||||||
|
export const supportsFeatureFromAttributes = (
|
||||||
|
attributes: {
|
||||||
|
[key: string]: any;
|
||||||
|
},
|
||||||
|
feature: number
|
||||||
): boolean =>
|
): boolean =>
|
||||||
// eslint-disable-next-line no-bitwise
|
// eslint-disable-next-line no-bitwise
|
||||||
(stateObj.attributes.supported_features! & feature) !== 0;
|
(attributes.supported_features! & feature) !== 0;
|
||||||
|
@@ -7,8 +7,11 @@ import { round } from "./round";
|
|||||||
* @param stateObj The entity state object
|
* @param stateObj The entity state object
|
||||||
*/
|
*/
|
||||||
export const isNumericState = (stateObj: HassEntity): boolean =>
|
export const isNumericState = (stateObj: HassEntity): boolean =>
|
||||||
!!stateObj.attributes.unit_of_measurement ||
|
isNumericFromAttributes(stateObj.attributes);
|
||||||
!!stateObj.attributes.state_class;
|
|
||||||
|
export const isNumericFromAttributes = (attributes: {
|
||||||
|
[key: string]: any;
|
||||||
|
}): boolean => !!attributes.unit_of_measurement || !!attributes.state_class;
|
||||||
|
|
||||||
export const numberFormatToLocale = (
|
export const numberFormatToLocale = (
|
||||||
localeOptions: FrontendLocaleData
|
localeOptions: FrontendLocaleData
|
||||||
|
@@ -13,7 +13,7 @@ export const throttle = <T extends any[]>(
|
|||||||
) => {
|
) => {
|
||||||
let timeout: number | undefined;
|
let timeout: number | undefined;
|
||||||
let previous = 0;
|
let previous = 0;
|
||||||
return (...args: T): void => {
|
const throttledFunc = (...args: T): void => {
|
||||||
const later = () => {
|
const later = () => {
|
||||||
previous = leading === false ? 0 : Date.now();
|
previous = leading === false ? 0 : Date.now();
|
||||||
timeout = undefined;
|
timeout = undefined;
|
||||||
@@ -35,4 +35,10 @@ export const throttle = <T extends any[]>(
|
|||||||
timeout = window.setTimeout(later, remaining);
|
timeout = window.setTimeout(later, remaining);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
throttledFunc.cancel = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = undefined;
|
||||||
|
previous = 0;
|
||||||
|
};
|
||||||
|
return throttledFunc;
|
||||||
};
|
};
|
||||||
|
@@ -34,7 +34,7 @@ import {
|
|||||||
endOfMonth,
|
endOfMonth,
|
||||||
endOfQuarter,
|
endOfQuarter,
|
||||||
endOfYear,
|
endOfYear,
|
||||||
} from "date-fns";
|
} from "date-fns/esm";
|
||||||
import {
|
import {
|
||||||
formatDate,
|
formatDate,
|
||||||
formatDateMonth,
|
formatDateMonth,
|
||||||
|
@@ -20,7 +20,7 @@ interface HassEntityWithCachedName extends HassEntity {
|
|||||||
friendly_name: string;
|
friendly_name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
||||||
|
|
||||||
// eslint-disable-next-line lit/prefer-static-styles
|
// eslint-disable-next-line lit/prefer-static-styles
|
||||||
const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) =>
|
const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) =>
|
||||||
|
@@ -2,12 +2,7 @@ import type { Button } from "@material/mwc-button";
|
|||||||
import "@material/mwc-menu";
|
import "@material/mwc-menu";
|
||||||
import type { Corner, Menu, MenuCorner } from "@material/mwc-menu";
|
import type { Corner, Menu, MenuCorner } from "@material/mwc-menu";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import {
|
import { customElement, property, query } from "lit/decorators";
|
||||||
customElement,
|
|
||||||
property,
|
|
||||||
query,
|
|
||||||
queryAssignedElements,
|
|
||||||
} from "lit/decorators";
|
|
||||||
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
|
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
|
||||||
import type { HaIconButton } from "./ha-icon-button";
|
import type { HaIconButton } from "./ha-icon-button";
|
||||||
|
|
||||||
@@ -33,12 +28,6 @@ export class HaButtonMenu extends LitElement {
|
|||||||
|
|
||||||
@query("mwc-menu", true) private _menu?: Menu;
|
@query("mwc-menu", true) private _menu?: Menu;
|
||||||
|
|
||||||
@queryAssignedElements({
|
|
||||||
slot: "trigger",
|
|
||||||
selector: "ha-icon-button, mwc-button",
|
|
||||||
})
|
|
||||||
private _triggerButton!: Array<HaIconButton | Button>;
|
|
||||||
|
|
||||||
public get items() {
|
public get items() {
|
||||||
return this._menu?.items;
|
return this._menu?.items;
|
||||||
}
|
}
|
||||||
@@ -51,14 +40,14 @@ export class HaButtonMenu extends LitElement {
|
|||||||
if (this._menu?.open) {
|
if (this._menu?.open) {
|
||||||
this._menu.focusItemAtIndex(0);
|
this._menu.focusItemAtIndex(0);
|
||||||
} else {
|
} else {
|
||||||
this._triggerButton[0]?.focus();
|
this._triggerButton?.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div @click=${this._handleClick}>
|
<div @click=${this._handleClick}>
|
||||||
<slot name="trigger"></slot>
|
<slot name="trigger" @slotchange=${this._setTriggerAria}></slot>
|
||||||
</div>
|
</div>
|
||||||
<mwc-menu
|
<mwc-menu
|
||||||
.corner=${this.corner}
|
.corner=${this.corner}
|
||||||
@@ -97,6 +86,18 @@ export class HaButtonMenu extends LitElement {
|
|||||||
this._menu!.show();
|
this._menu!.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get _triggerButton() {
|
||||||
|
return this.querySelector(
|
||||||
|
'ha-icon-button[slot="trigger"], mwc-button[slot="trigger"]'
|
||||||
|
) as HaIconButton | Button | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setTriggerAria() {
|
||||||
|
if (this._triggerButton) {
|
||||||
|
this._triggerButton.ariaHasPopup = "menu";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
:host {
|
:host {
|
||||||
|
@@ -66,9 +66,13 @@ export class HaChip extends LitElement {
|
|||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
|
color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
|
||||||
}
|
}
|
||||||
|
.mdc-chip.mdc-chip--selected .mdc-chip__checkmark,
|
||||||
.mdc-chip.no-text
|
.mdc-chip.no-text
|
||||||
.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) {
|
.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) {
|
||||||
margin-right: -4px;
|
margin-right: -4px;
|
||||||
|
margin-inline-start: -4px;
|
||||||
|
margin-inline-end: 4px;
|
||||||
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
span[role="gridcell"] {
|
span[role="gridcell"] {
|
||||||
|
@@ -3,6 +3,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
|||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { formatNumber } from "../common/number/format_number";
|
import { formatNumber } from "../common/number/format_number";
|
||||||
import { CLIMATE_PRESET_NONE } from "../data/climate";
|
import { CLIMATE_PRESET_NONE } from "../data/climate";
|
||||||
|
import { UNAVAILABLE_STATES } from "../data/entity";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
|
|
||||||
@customElement("ha-climate-state")
|
@customElement("ha-climate-state")
|
||||||
@@ -15,22 +16,22 @@ class HaClimateState extends LitElement {
|
|||||||
const currentStatus = this._computeCurrentStatus();
|
const currentStatus = this._computeCurrentStatus();
|
||||||
|
|
||||||
return html`<div class="target">
|
return html`<div class="target">
|
||||||
${this.stateObj.state !== "unknown"
|
${!UNAVAILABLE_STATES.includes(this.stateObj.state)
|
||||||
? html`<span class="state-label">
|
? html`<span class="state-label">
|
||||||
${this._localizeState()}
|
${this._localizeState()}
|
||||||
${this.stateObj.attributes.preset_mode &&
|
${this.stateObj.attributes.preset_mode &&
|
||||||
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
|
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
|
||||||
? html`-
|
? html`-
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
`state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}`
|
`state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}`
|
||||||
) || this.stateObj.attributes.preset_mode}`
|
) || this.stateObj.attributes.preset_mode}`
|
||||||
: ""}
|
: ""}
|
||||||
</span>`
|
</span>
|
||||||
: ""}
|
<div class="unit">${this._computeTarget()}</div>`
|
||||||
<div class="unit">${this._computeTarget()}</div>
|
: this._localizeState()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${currentStatus
|
${currentStatus && !UNAVAILABLE_STATES.includes(this.stateObj.state)
|
||||||
? html`<div class="current">
|
? html`<div class="current">
|
||||||
${this.hass.localize("ui.card.climate.currently")}:
|
${this.hass.localize("ui.card.climate.currently")}:
|
||||||
<div class="unit">${currentStatus}</div>
|
<div class="unit">${currentStatus}</div>
|
||||||
@@ -108,6 +109,10 @@ class HaClimateState extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _localizeState(): string {
|
private _localizeState(): string {
|
||||||
|
if (UNAVAILABLE_STATES.includes(this.stateObj.state)) {
|
||||||
|
return this.hass.localize(`state.default.${this.stateObj.state}`);
|
||||||
|
}
|
||||||
|
|
||||||
const stateString = this.hass.localize(
|
const stateString = this.hass.localize(
|
||||||
`component.climate.state._.${this.stateObj.state}`
|
`component.climate.state._.${this.stateObj.state}`
|
||||||
);
|
);
|
||||||
|
@@ -140,6 +140,9 @@ export class HaDateRangePicker extends LitElement {
|
|||||||
return css`
|
return css`
|
||||||
ha-svg-icon {
|
ha-svg-icon {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
margin-inline-start: initial;
|
||||||
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-range-inputs {
|
.date-range-inputs {
|
||||||
@@ -166,6 +169,9 @@ export class HaDateRangePicker extends LitElement {
|
|||||||
|
|
||||||
ha-textfield:last-child {
|
ha-textfield:last-child {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: initial;
|
||||||
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 800px) {
|
@media only screen and (max-width: 800px) {
|
||||||
|
@@ -133,6 +133,9 @@ class HaExpansionPanel extends LitElement {
|
|||||||
.summary-icon {
|
.summary-icon {
|
||||||
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
margin-inline-start: auto;
|
||||||
|
margin-inline-end: initial;
|
||||||
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-icon.expanded {
|
.summary-icon.expanded {
|
||||||
|
@@ -2,6 +2,7 @@ import "@material/mwc-icon-button";
|
|||||||
import type { IconButton } from "@material/mwc-icon-button";
|
import type { IconButton } from "@material/mwc-icon-button";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, query } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
|
|
||||||
@customElement("ha-icon-button")
|
@customElement("ha-icon-button")
|
||||||
@@ -12,7 +13,12 @@ export class HaIconButton extends LitElement {
|
|||||||
@property({ type: String }) path?: string;
|
@property({ type: String }) path?: string;
|
||||||
|
|
||||||
// Label that is used for ARIA support and as tooltip
|
// Label that is used for ARIA support and as tooltip
|
||||||
@property({ type: String }) label = "";
|
@property({ type: String }) label?: string;
|
||||||
|
|
||||||
|
// These should always be set as properties, not attributes,
|
||||||
|
// so that only the <button> element gets the attribute
|
||||||
|
@property({ type: String, attribute: "aria-haspopup" })
|
||||||
|
override ariaHasPopup!: IconButton["ariaHasPopup"];
|
||||||
|
|
||||||
@property({ type: Boolean }) hideTitle = false;
|
@property({ type: Boolean }) hideTitle = false;
|
||||||
|
|
||||||
@@ -28,11 +34,11 @@ export class HaIconButton extends LitElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
// Note: `ariaLabel` required despite the `mwc-icon-button` docs saying `label` should be enough
|
|
||||||
return html`
|
return html`
|
||||||
<mwc-icon-button
|
<mwc-icon-button
|
||||||
.ariaLabel=${this.label}
|
aria-label=${ifDefined(this.label)}
|
||||||
.title=${this.hideTitle ? "" : this.label}
|
title=${ifDefined(this.hideTitle ? undefined : this.label)}
|
||||||
|
aria-haspopup=${ifDefined(this.ariaHasPopup)}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
>
|
>
|
||||||
${this.path
|
${this.path
|
||||||
|
@@ -47,11 +47,19 @@ export class HaSelect extends SelectBase {
|
|||||||
.mdc-select__anchor {
|
.mdc-select__anchor {
|
||||||
width: var(--ha-select-min-width, 200px);
|
width: var(--ha-select-min-width, 200px);
|
||||||
}
|
}
|
||||||
.mdc-floating-label {
|
.mdc-select--filled .mdc-floating-label {
|
||||||
inset-inline-start: 16px !important;
|
inset-inline-start: 12px;
|
||||||
inset-inline-end: initial !important;
|
inset-inline-end: initial;
|
||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
|
.mdc-select .mdc-select__anchor {
|
||||||
|
padding-inline-start: 12px;
|
||||||
|
padding-inline-end: 0px;
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
.mdc-select__anchor .mdc-floating-label--float-above {
|
||||||
|
transform-origin: var(--float-start);
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -1,15 +1,24 @@
|
|||||||
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import { html, LitElement } from "lit";
|
import { html, LitElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
|
import memoizeOne from "memoize-one";
|
||||||
import { DeviceRegistryEntry } from "../../data/device_registry";
|
import { DeviceRegistryEntry } from "../../data/device_registry";
|
||||||
import { EntityRegistryEntry } from "../../data/entity_registry";
|
import {
|
||||||
|
EntityRegistryEntry,
|
||||||
|
subscribeEntityRegistry,
|
||||||
|
} from "../../data/entity_registry";
|
||||||
|
import {
|
||||||
|
EntitySources,
|
||||||
|
fetchEntitySourcesWithCache,
|
||||||
|
} from "../../data/entity_sources";
|
||||||
import { AreaSelector } from "../../data/selector";
|
import { AreaSelector } from "../../data/selector";
|
||||||
|
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import "../ha-area-picker";
|
import "../ha-area-picker";
|
||||||
import "../ha-areas-picker";
|
import "../ha-areas-picker";
|
||||||
|
|
||||||
@customElement("ha-selector-area")
|
@customElement("ha-selector-area")
|
||||||
export class HaAreaSelector extends LitElement {
|
export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
||||||
@property() public hass!: HomeAssistant;
|
@property() public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() public selector!: AreaSelector;
|
@property() public selector!: AreaSelector;
|
||||||
@@ -20,29 +29,44 @@ export class HaAreaSelector extends LitElement {
|
|||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
@state() public _configEntries?: ConfigEntry[];
|
@state() private _entitySources?: EntitySources;
|
||||||
|
|
||||||
|
@state() private _entities?: EntityRegistryEntry[];
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
@property({ type: Boolean }) public required = true;
|
@property({ type: Boolean }) public required = true;
|
||||||
|
|
||||||
|
public hassSubscribe(): UnsubscribeFunc[] {
|
||||||
|
return [
|
||||||
|
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||||
|
this._entities = entities.filter((entity) => entity.device_id !== null);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
protected updated(changedProperties) {
|
protected updated(changedProperties) {
|
||||||
if (changedProperties.has("selector")) {
|
if (
|
||||||
const oldSelector = changedProperties.get("selector");
|
changedProperties.has("selector") &&
|
||||||
if (
|
(this.selector.area.device?.integration ||
|
||||||
oldSelector !== this.selector &&
|
this.selector.area.entity?.integration) &&
|
||||||
this.selector.area.device?.integration
|
!this._entitySources
|
||||||
) {
|
) {
|
||||||
getConfigEntries(this.hass, {
|
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||||
domain: this.selector.area.device.integration,
|
this._entitySources = sources;
|
||||||
}).then((entries) => {
|
});
|
||||||
this._configEntries = entries;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
|
if (
|
||||||
|
(this.selector.area.device?.integration ||
|
||||||
|
this.selector.area.entity?.integration) &&
|
||||||
|
!this._entitySources
|
||||||
|
) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.selector.area.multiple) {
|
if (!this.selector.area.multiple) {
|
||||||
return html`
|
return html`
|
||||||
<ha-area-picker
|
<ha-area-picker
|
||||||
@@ -87,39 +111,62 @@ export class HaAreaSelector extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _filterEntities = (entity: EntityRegistryEntry): boolean => {
|
private _filterEntities = (entity: EntityRegistryEntry): boolean => {
|
||||||
if (this.selector.area.entity?.integration) {
|
const filterIntegration = this.selector.area.entity?.integration;
|
||||||
if (entity.platform !== this.selector.area.entity.integration) {
|
if (
|
||||||
|
filterIntegration &&
|
||||||
|
this._entitySources?.[entity.entity_id]?.domain !== filterIntegration
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||||
|
if (!this.selector.area.device) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
manufacturer: filterManufacturer,
|
||||||
|
model: filterModel,
|
||||||
|
integration: filterIntegration,
|
||||||
|
} = this.selector.area.device;
|
||||||
|
|
||||||
|
if (filterManufacturer && device.manufacturer !== filterManufacturer) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (filterModel && device.model !== filterModel) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (filterIntegration && this._entitySources && this._entities) {
|
||||||
|
const deviceIntegrations = this._deviceIntegrations(
|
||||||
|
this._entitySources,
|
||||||
|
this._entities
|
||||||
|
);
|
||||||
|
if (!deviceIntegrations?.[device.id]?.includes(filterIntegration)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
private _deviceIntegrations = memoizeOne(
|
||||||
if (
|
(entitySources: EntitySources, entities: EntityRegistryEntry[]) => {
|
||||||
this.selector.area.device?.manufacturer &&
|
const deviceIntegrations: Record<string, string[]> = {};
|
||||||
device.manufacturer !== this.selector.area.device.manufacturer
|
|
||||||
) {
|
for (const entity of entities) {
|
||||||
return false;
|
const source = entitySources[entity.entity_id];
|
||||||
}
|
if (!source?.domain) {
|
||||||
if (
|
continue;
|
||||||
this.selector.area.device?.model &&
|
}
|
||||||
device.model !== this.selector.area.device.model
|
if (!deviceIntegrations[entity.device_id!]) {
|
||||||
) {
|
deviceIntegrations[entity.device_id!] = [];
|
||||||
return false;
|
}
|
||||||
}
|
deviceIntegrations[entity.device_id!].push(source.domain);
|
||||||
if (this.selector.area.device?.integration) {
|
|
||||||
if (
|
|
||||||
this._configEntries &&
|
|
||||||
!this._configEntries.some((entry) =>
|
|
||||||
device.config_entries.includes(entry.entry_id)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return deviceIntegrations;
|
||||||
}
|
}
|
||||||
return true;
|
);
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@@ -1,18 +1,33 @@
|
|||||||
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import { html, LitElement } from "lit";
|
import { html, LitElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
|
import memoizeOne from "memoize-one";
|
||||||
|
import { ConfigEntry } from "../../data/config_entries";
|
||||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||||
|
import {
|
||||||
|
EntityRegistryEntry,
|
||||||
|
subscribeEntityRegistry,
|
||||||
|
} from "../../data/entity_registry";
|
||||||
|
import {
|
||||||
|
EntitySources,
|
||||||
|
fetchEntitySourcesWithCache,
|
||||||
|
} from "../../data/entity_sources";
|
||||||
import type { DeviceSelector } from "../../data/selector";
|
import type { DeviceSelector } from "../../data/selector";
|
||||||
|
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "../device/ha-device-picker";
|
import "../device/ha-device-picker";
|
||||||
import "../device/ha-devices-picker";
|
import "../device/ha-devices-picker";
|
||||||
|
|
||||||
@customElement("ha-selector-device")
|
@customElement("ha-selector-device")
|
||||||
export class HaDeviceSelector extends LitElement {
|
export class HaDeviceSelector extends SubscribeMixin(LitElement) {
|
||||||
@property() public hass!: HomeAssistant;
|
@property() public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() public selector!: DeviceSelector;
|
@property() public selector!: DeviceSelector;
|
||||||
|
|
||||||
|
@state() private _entitySources?: EntitySources;
|
||||||
|
|
||||||
|
@state() private _entities?: EntityRegistryEntry[];
|
||||||
|
|
||||||
@property() public value?: any;
|
@property() public value?: any;
|
||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
@@ -25,20 +40,32 @@ export class HaDeviceSelector extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public required = true;
|
@property({ type: Boolean }) public required = true;
|
||||||
|
|
||||||
protected updated(changedProperties) {
|
public hassSubscribe(): UnsubscribeFunc[] {
|
||||||
if (changedProperties.has("selector")) {
|
return [
|
||||||
const oldSelector = changedProperties.get("selector");
|
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||||
if (oldSelector !== this.selector && this.selector.device?.integration) {
|
this._entities = entities.filter((entity) => entity.device_id !== null);
|
||||||
getConfigEntries(this.hass, {
|
}),
|
||||||
domain: this.selector.device.integration,
|
];
|
||||||
}).then((entries) => {
|
}
|
||||||
this._configEntries = entries;
|
|
||||||
});
|
protected updated(changedProperties): void {
|
||||||
}
|
super.updated(changedProperties);
|
||||||
|
if (
|
||||||
|
changedProperties.has("selector") &&
|
||||||
|
this.selector.device.integration &&
|
||||||
|
!this._entitySources
|
||||||
|
) {
|
||||||
|
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||||
|
this._entitySources = sources;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
|
if (this.selector.device.integration && !this._entitySources) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.selector.device.multiple) {
|
if (!this.selector.device.multiple) {
|
||||||
return html`
|
return html`
|
||||||
<ha-device-picker
|
<ha-device-picker
|
||||||
@@ -80,30 +107,48 @@ export class HaDeviceSelector extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||||
if (
|
const {
|
||||||
this.selector.device?.manufacturer &&
|
manufacturer: filterManufacturer,
|
||||||
device.manufacturer !== this.selector.device.manufacturer
|
model: filterModel,
|
||||||
) {
|
integration: filterIntegration,
|
||||||
|
} = this.selector.device;
|
||||||
|
|
||||||
|
if (filterManufacturer && device.manufacturer !== filterManufacturer) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (
|
if (filterModel && device.model !== filterModel) {
|
||||||
this.selector.device?.model &&
|
|
||||||
device.model !== this.selector.device.model
|
|
||||||
) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (this.selector.device?.integration) {
|
if (filterIntegration && this._entitySources && this._entities) {
|
||||||
if (
|
const deviceIntegrations = this._deviceIntegrations(
|
||||||
this._configEntries &&
|
this._entitySources,
|
||||||
!this._configEntries.some((entry) =>
|
this._entities
|
||||||
device.config_entries.includes(entry.entry_id)
|
);
|
||||||
)
|
if (!deviceIntegrations?.[device.id]?.includes(filterIntegration)) {
|
||||||
) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private _deviceIntegrations = memoizeOne(
|
||||||
|
(entitySources: EntitySources, entities: EntityRegistryEntry[]) => {
|
||||||
|
const deviceIntegrations: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
for (const entity of entities) {
|
||||||
|
const source = entitySources[entity.entity_id];
|
||||||
|
if (!source?.domain) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deviceIntegrations[entity.device_id!]) {
|
||||||
|
deviceIntegrations[entity.device_id!] = [];
|
||||||
|
}
|
||||||
|
deviceIntegrations[entity.device_id!].push(source.domain);
|
||||||
|
}
|
||||||
|
return deviceIntegrations;
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@@ -287,9 +287,7 @@ export class HaServiceControl extends LitElement {
|
|||||||
${shouldRenderServiceDataYaml
|
${shouldRenderServiceDataYaml
|
||||||
? html`<ha-yaml-editor
|
? html`<ha-yaml-editor
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize("ui.components.service-control.data")}
|
||||||
"ui.components.service-control.service_data"
|
|
||||||
)}
|
|
||||||
.name=${"data"}
|
.name=${"data"}
|
||||||
.defaultValue=${this._value?.data}
|
.defaultValue=${this._value?.data}
|
||||||
@value-changed=${this._dataChanged}
|
@value-changed=${this._dataChanged}
|
||||||
|
@@ -569,6 +569,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
height: 16px;
|
height: 16px;
|
||||||
--mdc-icon-size: 14px;
|
--mdc-icon-size: 14px;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
|
margin-inline-start: 4px !important;
|
||||||
|
margin-inline-end: -4px !important;
|
||||||
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
.mdc-chip__icon--leading {
|
.mdc-chip__icon--leading {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -578,6 +581,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
margin-left: -14px !important;
|
margin-left: -14px !important;
|
||||||
|
margin-inline-start: -14px !important;
|
||||||
|
margin-inline-end: 4px !important;
|
||||||
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
.expand-btn {
|
.expand-btn {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
@@ -616,11 +622,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
opacity: var(--light-disabled-opacity);
|
opacity: var(--light-disabled-opacity);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.mdc-chip__icon {
|
|
||||||
margin-inline-start: -14px !important;
|
|
||||||
margin-inline-end: 4px !important;
|
|
||||||
direction: var(--direction);
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -57,6 +57,9 @@ export class HaTextField extends TextFieldBase {
|
|||||||
.mdc-text-field__affix--suffix {
|
.mdc-text-field__affix--suffix {
|
||||||
padding-left: var(--text-field-suffix-padding-left, 12px);
|
padding-left: var(--text-field-suffix-padding-left, 12px);
|
||||||
padding-right: var(--text-field-suffix-padding-right, 0px);
|
padding-right: var(--text-field-suffix-padding-right, 0px);
|
||||||
|
padding-inline-start: var(--text-field-suffix-padding-left, 12px);
|
||||||
|
padding-inline-end: var(--text-field-suffix-padding-right, 0px);
|
||||||
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdc-text-field:not(.mdc-text-field--disabled)
|
.mdc-text-field:not(.mdc-text-field--disabled)
|
||||||
@@ -95,6 +98,7 @@ export class HaTextField extends TextFieldBase {
|
|||||||
.mdc-floating-label {
|
.mdc-floating-label {
|
||||||
inset-inline-start: 16px !important;
|
inset-inline-start: 16px !important;
|
||||||
inset-inline-end: initial !important;
|
inset-inline-end: initial !important;
|
||||||
|
transform-origin: var(--float-start);
|
||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,7 +3,7 @@ import { customElement, property } from "lit/decorators";
|
|||||||
import { LogbookEntry } from "../../data/logbook";
|
import { LogbookEntry } from "../../data/logbook";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import "./hat-logbook-note";
|
import "./hat-logbook-note";
|
||||||
import "../../panels/logbook/ha-logbook";
|
import "../../panels/logbook/ha-logbook-renderer";
|
||||||
import { TraceExtended } from "../../data/trace";
|
import { TraceExtended } from "../../data/trace";
|
||||||
|
|
||||||
@customElement("ha-trace-logbook")
|
@customElement("ha-trace-logbook")
|
||||||
@@ -19,12 +19,12 @@ export class HaTraceLogbook extends LitElement {
|
|||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return this.logbookEntries.length
|
return this.logbookEntries.length
|
||||||
? html`
|
? html`
|
||||||
<ha-logbook
|
<ha-logbook-renderer
|
||||||
relative-time
|
relative-time
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.entries=${this.logbookEntries}
|
.entries=${this.logbookEntries}
|
||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
></ha-logbook>
|
></ha-logbook-renderer>
|
||||||
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
|
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
|
||||||
`
|
`
|
||||||
: html`<div class="padded-box">
|
: html`<div class="padded-box">
|
||||||
|
@@ -13,7 +13,7 @@ import {
|
|||||||
getDataFromPath,
|
getDataFromPath,
|
||||||
TraceExtended,
|
TraceExtended,
|
||||||
} from "../../data/trace";
|
} from "../../data/trace";
|
||||||
import "../../panels/logbook/ha-logbook";
|
import "../../panels/logbook/ha-logbook-renderer";
|
||||||
import { traceTabStyles } from "./trace-tab-styles";
|
import { traceTabStyles } from "./trace-tab-styles";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import type { NodeInfo } from "./hat-script-graph";
|
import type { NodeInfo } from "./hat-script-graph";
|
||||||
@@ -194,7 +194,7 @@ export class HaTracePathDetails extends LitElement {
|
|||||||
// it's the last entry. Find all logbook entries after start.
|
// it's the last entry. Find all logbook entries after start.
|
||||||
const startTime = new Date(startTrace[0].timestamp);
|
const startTime = new Date(startTrace[0].timestamp);
|
||||||
const idx = this.logbookEntries.findIndex(
|
const idx = this.logbookEntries.findIndex(
|
||||||
(entry) => new Date(entry.when) >= startTime
|
(entry) => new Date(entry.when * 1000) >= startTime
|
||||||
);
|
);
|
||||||
if (idx === -1) {
|
if (idx === -1) {
|
||||||
entries = [];
|
entries = [];
|
||||||
@@ -210,7 +210,7 @@ export class HaTracePathDetails extends LitElement {
|
|||||||
entries = [];
|
entries = [];
|
||||||
|
|
||||||
for (const entry of this.logbookEntries || []) {
|
for (const entry of this.logbookEntries || []) {
|
||||||
const entryDate = new Date(entry.when);
|
const entryDate = new Date(entry.when * 1000);
|
||||||
if (entryDate >= startTime) {
|
if (entryDate >= startTime) {
|
||||||
if (entryDate < endTime) {
|
if (entryDate < endTime) {
|
||||||
entries.push(entry);
|
entries.push(entry);
|
||||||
@@ -224,12 +224,12 @@ export class HaTracePathDetails extends LitElement {
|
|||||||
|
|
||||||
return entries.length
|
return entries.length
|
||||||
? html`
|
? html`
|
||||||
<ha-logbook
|
<ha-logbook-renderer
|
||||||
relative-time
|
relative-time
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.entries=${entries}
|
.entries=${entries}
|
||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
></ha-logbook>
|
></ha-logbook-renderer>
|
||||||
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
|
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
|
||||||
`
|
`
|
||||||
: html`<div class="padded-box">
|
: html`<div class="padded-box">
|
||||||
|
@@ -116,7 +116,7 @@ class LogbookRenderer {
|
|||||||
maybeRenderItem() {
|
maybeRenderItem() {
|
||||||
const logbookEntry = this.curItem;
|
const logbookEntry = this.curItem;
|
||||||
this.curIndex++;
|
this.curIndex++;
|
||||||
const entryDate = new Date(logbookEntry.when);
|
const entryDate = new Date(logbookEntry.when * 1000);
|
||||||
|
|
||||||
if (this.pendingItems.length === 0) {
|
if (this.pendingItems.length === 0) {
|
||||||
this.pendingItems.push([entryDate, logbookEntry]);
|
this.pendingItems.push([entryDate, logbookEntry]);
|
||||||
@@ -248,7 +248,7 @@ class ActionRenderer {
|
|||||||
// Render all logbook items that are in front of this item.
|
// Render all logbook items that are in front of this item.
|
||||||
while (
|
while (
|
||||||
this.logbookRenderer.hasNext &&
|
this.logbookRenderer.hasNext &&
|
||||||
new Date(this.logbookRenderer.curItem.when) < timestamp
|
new Date(this.logbookRenderer.curItem.when * 1000) < timestamp
|
||||||
) {
|
) {
|
||||||
this.logbookRenderer.maybeRenderItem();
|
this.logbookRenderer.maybeRenderItem();
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,7 @@ export interface ApplicationCredential {
|
|||||||
domain: string;
|
domain: string;
|
||||||
client_id: string;
|
client_id: string;
|
||||||
client_secret: string;
|
client_secret: string;
|
||||||
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchApplicationCredentialsConfig = async (hass: HomeAssistant) =>
|
export const fetchApplicationCredentialsConfig = async (hass: HomeAssistant) =>
|
||||||
@@ -25,13 +26,15 @@ export const createApplicationCredential = async (
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
domain: string,
|
domain: string,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
clientSecret: string
|
clientSecret: string,
|
||||||
|
name?: string
|
||||||
) =>
|
) =>
|
||||||
hass.callWS<ApplicationCredential>({
|
hass.callWS<ApplicationCredential>({
|
||||||
type: "application_credentials/create",
|
type: "application_credentials/create",
|
||||||
domain,
|
domain,
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
client_secret: clientSecret,
|
client_secret: clientSecret,
|
||||||
|
name,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deleteApplicationCredential = async (
|
export const deleteApplicationCredential = async (
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
|
||||||
import { LocalizeFunc } from "../common/translations/localize";
|
import { LocalizeFunc } from "../common/translations/localize";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import {
|
import {
|
||||||
computeHistory,
|
computeHistory,
|
||||||
fetchRecent,
|
HistoryStates,
|
||||||
HistoryResult,
|
HistoryResult,
|
||||||
LineChartUnit,
|
LineChartUnit,
|
||||||
TimelineEntity,
|
TimelineEntity,
|
||||||
entityIdHistoryNeedsAttributes,
|
entityIdHistoryNeedsAttributes,
|
||||||
|
fetchRecentWS,
|
||||||
} from "./history";
|
} from "./history";
|
||||||
|
|
||||||
export interface CacheConfig {
|
export interface CacheConfig {
|
||||||
@@ -34,7 +34,7 @@ const RECENT_THRESHOLD = 60000; // 1 minute
|
|||||||
const RECENT_CACHE: { [cacheKey: string]: RecentCacheResults } = {};
|
const RECENT_CACHE: { [cacheKey: string]: RecentCacheResults } = {};
|
||||||
const stateHistoryCache: { [cacheKey: string]: CachedResults } = {};
|
const stateHistoryCache: { [cacheKey: string]: CachedResults } = {};
|
||||||
|
|
||||||
// Cached type 1 unction. Without cache config.
|
// Cached type 1 function. Without cache config.
|
||||||
export const getRecent = (
|
export const getRecent = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityId: string,
|
entityId: string,
|
||||||
@@ -55,7 +55,7 @@ export const getRecent = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
|
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
|
||||||
const prom = fetchRecent(
|
const prom = fetchRecentWS(
|
||||||
hass,
|
hass,
|
||||||
entityId,
|
entityId,
|
||||||
startTime,
|
startTime,
|
||||||
@@ -103,13 +103,14 @@ export const getRecentWithCache = (
|
|||||||
language: string
|
language: string
|
||||||
) => {
|
) => {
|
||||||
const cacheKey = cacheConfig.cacheKey;
|
const cacheKey = cacheConfig.cacheKey;
|
||||||
|
const fullCacheKey = cacheKey + `_${cacheConfig.hoursToShow}`;
|
||||||
const endTime = new Date();
|
const endTime = new Date();
|
||||||
const startTime = new Date(endTime);
|
const startTime = new Date(endTime);
|
||||||
startTime.setHours(startTime.getHours() - cacheConfig.hoursToShow);
|
startTime.setHours(startTime.getHours() - cacheConfig.hoursToShow);
|
||||||
let toFetchStartTime = startTime;
|
let toFetchStartTime = startTime;
|
||||||
let appendingToCache = false;
|
let appendingToCache = false;
|
||||||
|
|
||||||
let cache = stateHistoryCache[cacheKey + `_${cacheConfig.hoursToShow}`];
|
let cache = stateHistoryCache[fullCacheKey];
|
||||||
if (
|
if (
|
||||||
cache &&
|
cache &&
|
||||||
toFetchStartTime >= cache.startTime &&
|
toFetchStartTime >= cache.startTime &&
|
||||||
@@ -123,7 +124,7 @@ export const getRecentWithCache = (
|
|||||||
return cache.prom;
|
return cache.prom;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
cache = stateHistoryCache[cacheKey] = getEmptyCache(
|
cache = stateHistoryCache[fullCacheKey] = getEmptyCache(
|
||||||
language,
|
language,
|
||||||
startTime,
|
startTime,
|
||||||
endTime
|
endTime
|
||||||
@@ -134,12 +135,12 @@ export const getRecentWithCache = (
|
|||||||
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
|
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
|
||||||
|
|
||||||
const genProm = async () => {
|
const genProm = async () => {
|
||||||
let fetchedHistory: HassEntity[][];
|
let fetchedHistory: HistoryStates;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const results = await Promise.all([
|
const results = await Promise.all([
|
||||||
curCacheProm,
|
curCacheProm,
|
||||||
fetchRecent(
|
fetchRecentWS(
|
||||||
hass,
|
hass,
|
||||||
entityId,
|
entityId,
|
||||||
toFetchStartTime,
|
toFetchStartTime,
|
||||||
@@ -152,7 +153,7 @@ export const getRecentWithCache = (
|
|||||||
]);
|
]);
|
||||||
fetchedHistory = results[1];
|
fetchedHistory = results[1];
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
delete stateHistoryCache[cacheKey];
|
delete stateHistoryCache[fullCacheKey];
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
const stateHistory = computeHistory(hass, fetchedHistory, localize);
|
const stateHistory = computeHistory(hass, fetchedHistory, localize);
|
||||||
|
@@ -1,11 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
|
addDays,
|
||||||
addHours,
|
addHours,
|
||||||
|
addMilliseconds,
|
||||||
|
addMonths,
|
||||||
differenceInDays,
|
differenceInDays,
|
||||||
endOfToday,
|
endOfToday,
|
||||||
endOfYesterday,
|
endOfYesterday,
|
||||||
startOfToday,
|
startOfToday,
|
||||||
startOfYesterday,
|
startOfYesterday,
|
||||||
} from "date-fns";
|
} from "date-fns/esm";
|
||||||
import { Collection, getCollection } from "home-assistant-js-websocket";
|
import { Collection, getCollection } from "home-assistant-js-websocket";
|
||||||
import { groupBy } from "../common/util/group-by";
|
import { groupBy } from "../common/util/group-by";
|
||||||
import { subscribeOne } from "../common/util/subscribe-one";
|
import { subscribeOne } from "../common/util/subscribe-one";
|
||||||
@@ -14,9 +17,9 @@ import { ConfigEntry, getConfigEntries } from "./config_entries";
|
|||||||
import { subscribeEntityRegistry } from "./entity_registry";
|
import { subscribeEntityRegistry } from "./entity_registry";
|
||||||
import {
|
import {
|
||||||
fetchStatistics,
|
fetchStatistics,
|
||||||
|
getStatisticMetadata,
|
||||||
Statistics,
|
Statistics,
|
||||||
StatisticsMetaData,
|
StatisticsMetaData,
|
||||||
getStatisticMetadata,
|
|
||||||
} from "./history";
|
} from "./history";
|
||||||
|
|
||||||
const energyCollectionKeys: (string | undefined)[] = [];
|
const energyCollectionKeys: (string | undefined)[] = [];
|
||||||
@@ -232,19 +235,24 @@ export const energySourcesByType = (prefs: EnergyPreferences) =>
|
|||||||
export interface EnergyData {
|
export interface EnergyData {
|
||||||
start: Date;
|
start: Date;
|
||||||
end?: Date;
|
end?: Date;
|
||||||
|
startCompare?: Date;
|
||||||
|
endCompare?: Date;
|
||||||
prefs: EnergyPreferences;
|
prefs: EnergyPreferences;
|
||||||
info: EnergyInfo;
|
info: EnergyInfo;
|
||||||
stats: Statistics;
|
stats: Statistics;
|
||||||
|
statsCompare: Statistics;
|
||||||
co2SignalConfigEntry?: ConfigEntry;
|
co2SignalConfigEntry?: ConfigEntry;
|
||||||
co2SignalEntity?: string;
|
co2SignalEntity?: string;
|
||||||
fossilEnergyConsumption?: FossilEnergyConsumption;
|
fossilEnergyConsumption?: FossilEnergyConsumption;
|
||||||
|
fossilEnergyConsumptionCompare?: FossilEnergyConsumption;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getEnergyData = async (
|
const getEnergyData = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
prefs: EnergyPreferences,
|
prefs: EnergyPreferences,
|
||||||
start: Date,
|
start: Date,
|
||||||
end?: Date
|
end?: Date,
|
||||||
|
compare?: boolean
|
||||||
): Promise<EnergyData> => {
|
): Promise<EnergyData> => {
|
||||||
const [configEntries, entityRegistryEntries, info] = await Promise.all([
|
const [configEntries, entityRegistryEntries, info] = await Promise.all([
|
||||||
getConfigEntries(hass, { domain: "co2signal" }),
|
getConfigEntries(hass, { domain: "co2signal" }),
|
||||||
@@ -350,6 +358,8 @@ const getEnergyData = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dayDifference = differenceInDays(end || new Date(), start);
|
const dayDifference = differenceInDays(end || new Date(), start);
|
||||||
|
const period =
|
||||||
|
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour";
|
||||||
|
|
||||||
// Subtract 1 hour from start to get starting point data
|
// Subtract 1 hour from start to get starting point data
|
||||||
const startMinHour = addHours(start, -1);
|
const startMinHour = addHours(start, -1);
|
||||||
@@ -359,10 +369,34 @@ const getEnergyData = async (
|
|||||||
startMinHour,
|
startMinHour,
|
||||||
end,
|
end,
|
||||||
statIDs,
|
statIDs,
|
||||||
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
|
period
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let statsCompare;
|
||||||
|
let startCompare;
|
||||||
|
let endCompare;
|
||||||
|
if (compare) {
|
||||||
|
if (dayDifference > 27 && dayDifference < 32) {
|
||||||
|
// When comparing a month, we want to start at the begining of the month
|
||||||
|
startCompare = addMonths(start, -1);
|
||||||
|
} else {
|
||||||
|
startCompare = addDays(start, (dayDifference + 1) * -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const compareStartMinHour = addHours(startCompare, -1);
|
||||||
|
endCompare = addMilliseconds(start, -1);
|
||||||
|
|
||||||
|
statsCompare = await fetchStatistics(
|
||||||
|
hass!,
|
||||||
|
compareStartMinHour,
|
||||||
|
endCompare,
|
||||||
|
statIDs,
|
||||||
|
period
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let fossilEnergyConsumption: FossilEnergyConsumption | undefined;
|
let fossilEnergyConsumption: FossilEnergyConsumption | undefined;
|
||||||
|
let fossilEnergyConsumptionCompare: FossilEnergyConsumption | undefined;
|
||||||
|
|
||||||
if (co2SignalEntity !== undefined) {
|
if (co2SignalEntity !== undefined) {
|
||||||
fossilEnergyConsumption = await getFossilEnergyConsumption(
|
fossilEnergyConsumption = await getFossilEnergyConsumption(
|
||||||
@@ -373,6 +407,16 @@ const getEnergyData = async (
|
|||||||
end,
|
end,
|
||||||
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
|
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
|
||||||
);
|
);
|
||||||
|
if (compare) {
|
||||||
|
fossilEnergyConsumptionCompare = await getFossilEnergyConsumption(
|
||||||
|
hass!,
|
||||||
|
startCompare,
|
||||||
|
consumptionStatIDs,
|
||||||
|
co2SignalEntity,
|
||||||
|
endCompare,
|
||||||
|
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.values(stats).forEach((stat) => {
|
Object.values(stats).forEach((stat) => {
|
||||||
@@ -388,15 +432,19 @@ const getEnergyData = async (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = {
|
const data: EnergyData = {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
|
startCompare,
|
||||||
|
endCompare,
|
||||||
info,
|
info,
|
||||||
prefs,
|
prefs,
|
||||||
stats,
|
stats,
|
||||||
|
statsCompare,
|
||||||
co2SignalConfigEntry,
|
co2SignalConfigEntry,
|
||||||
co2SignalEntity,
|
co2SignalEntity,
|
||||||
fossilEnergyConsumption,
|
fossilEnergyConsumption,
|
||||||
|
fossilEnergyConsumptionCompare,
|
||||||
};
|
};
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -405,9 +453,11 @@ const getEnergyData = async (
|
|||||||
export interface EnergyCollection extends Collection<EnergyData> {
|
export interface EnergyCollection extends Collection<EnergyData> {
|
||||||
start: Date;
|
start: Date;
|
||||||
end?: Date;
|
end?: Date;
|
||||||
|
compare?: boolean;
|
||||||
prefs?: EnergyPreferences;
|
prefs?: EnergyPreferences;
|
||||||
clearPrefs(): void;
|
clearPrefs(): void;
|
||||||
setPeriod(newStart: Date, newEnd?: Date): void;
|
setPeriod(newStart: Date, newEnd?: Date): void;
|
||||||
|
setCompare(compare: boolean): void;
|
||||||
_refreshTimeout?: number;
|
_refreshTimeout?: number;
|
||||||
_updatePeriodTimeout?: number;
|
_updatePeriodTimeout?: number;
|
||||||
_active: number;
|
_active: number;
|
||||||
@@ -478,7 +528,8 @@ export const getEnergyDataCollection = (
|
|||||||
hass,
|
hass,
|
||||||
collection.prefs,
|
collection.prefs,
|
||||||
collection.start,
|
collection.start,
|
||||||
collection.end
|
collection.end,
|
||||||
|
collection.compare
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
) as EnergyCollection;
|
) as EnergyCollection;
|
||||||
@@ -534,6 +585,9 @@ export const getEnergyDataCollection = (
|
|||||||
collection._updatePeriodTimeout = undefined;
|
collection._updatePeriodTimeout = undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
collection.setCompare = (compare: boolean) => {
|
||||||
|
collection.compare = compare;
|
||||||
|
};
|
||||||
return collection;
|
return collection;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -20,3 +20,20 @@ export const BOARD_NAMES: Record<string, string> = {
|
|||||||
"intel-nuc": "Intel NUC",
|
"intel-nuc": "Intel NUC",
|
||||||
yellow: "Home Assistant Yellow",
|
yellow: "Home Assistant Yellow",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface HardwareInfo {
|
||||||
|
hardware: HardwareInfoEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HardwareInfoEntry {
|
||||||
|
board: HardwareInfoBoardInfo;
|
||||||
|
name: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HardwareInfoBoardInfo {
|
||||||
|
manufacturer: string;
|
||||||
|
model?: string;
|
||||||
|
revision?: string;
|
||||||
|
hassio_board_id?: string;
|
||||||
|
}
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
import { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display";
|
||||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
import { computeStateNameFromEntityAttributes } from "../common/entity/compute_state_name";
|
||||||
import { computeStateName } from "../common/entity/compute_state_name";
|
|
||||||
import { LocalizeFunc } from "../common/translations/localize";
|
import { LocalizeFunc } from "../common/translations/localize";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import { FrontendLocaleData } from "./translation";
|
import { FrontendLocaleData } from "./translation";
|
||||||
@@ -27,7 +26,7 @@ const LINE_ATTRIBUTES_TO_KEEP = [
|
|||||||
|
|
||||||
export interface LineChartState {
|
export interface LineChartState {
|
||||||
state: string;
|
state: string;
|
||||||
last_changed: string;
|
last_changed: number;
|
||||||
attributes?: Record<string, any>;
|
attributes?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +46,7 @@ export interface LineChartUnit {
|
|||||||
export interface TimelineState {
|
export interface TimelineState {
|
||||||
state_localize: string;
|
state_localize: string;
|
||||||
state: string;
|
state: string;
|
||||||
last_changed: string;
|
last_changed: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TimelineEntity {
|
export interface TimelineEntity {
|
||||||
@@ -141,6 +140,21 @@ export interface StatisticsValidationResults {
|
|||||||
[statisticId: string]: StatisticsValidationResult[];
|
[statisticId: string]: StatisticsValidationResult[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HistoryStates {
|
||||||
|
[entityId: string]: EntityHistoryState[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntityHistoryState {
|
||||||
|
/** state */
|
||||||
|
s: string;
|
||||||
|
/** attributes */
|
||||||
|
a: { [key: string]: any };
|
||||||
|
/** last_changed; if set, also applies to lu */
|
||||||
|
lc: number;
|
||||||
|
/** last_updated */
|
||||||
|
lu: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const entityIdHistoryNeedsAttributes = (
|
export const entityIdHistoryNeedsAttributes = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityId: string
|
entityId: string
|
||||||
@@ -181,6 +195,27 @@ export const fetchRecent = (
|
|||||||
return hass.callApi("GET", url);
|
return hass.callApi("GET", url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchRecentWS = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entityId: string, // This may be CSV
|
||||||
|
startTime: Date,
|
||||||
|
endTime: Date,
|
||||||
|
skipInitialState = false,
|
||||||
|
significantChangesOnly?: boolean,
|
||||||
|
minimalResponse = true,
|
||||||
|
noAttributes?: boolean
|
||||||
|
) =>
|
||||||
|
hass.callWS<HistoryStates>({
|
||||||
|
type: "history/history_during_period",
|
||||||
|
start_time: startTime.toISOString(),
|
||||||
|
end_time: endTime.toISOString(),
|
||||||
|
significant_changes_only: significantChangesOnly || false,
|
||||||
|
include_start_time_state: !skipInitialState,
|
||||||
|
minimal_response: minimalResponse,
|
||||||
|
no_attributes: noAttributes || false,
|
||||||
|
entity_ids: entityId.split(","),
|
||||||
|
});
|
||||||
|
|
||||||
export const fetchDate = (
|
export const fetchDate = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
startTime: Date,
|
startTime: Date,
|
||||||
@@ -198,6 +233,27 @@ export const fetchDate = (
|
|||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const fetchDateWS = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
startTime: Date,
|
||||||
|
endTime: Date,
|
||||||
|
entityId?: string
|
||||||
|
) => {
|
||||||
|
const params = {
|
||||||
|
type: "history/history_during_period",
|
||||||
|
start_time: startTime.toISOString(),
|
||||||
|
end_time: endTime.toISOString(),
|
||||||
|
minimal_response: true,
|
||||||
|
no_attributes: !!(
|
||||||
|
entityId && !entityIdHistoryNeedsAttributes(hass, entityId)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
if (entityId) {
|
||||||
|
return hass.callWS<HistoryStates>({ ...params, entity_ids: [entityId] });
|
||||||
|
}
|
||||||
|
return hass.callWS<HistoryStates>(params);
|
||||||
|
};
|
||||||
|
|
||||||
const equalState = (obj1: LineChartState, obj2: LineChartState) =>
|
const equalState = (obj1: LineChartState, obj2: LineChartState) =>
|
||||||
obj1.state === obj2.state &&
|
obj1.state === obj2.state &&
|
||||||
// Only compare attributes if both states have an attributes object.
|
// Only compare attributes if both states have an attributes object.
|
||||||
@@ -212,46 +268,47 @@ const equalState = (obj1: LineChartState, obj2: LineChartState) =>
|
|||||||
const processTimelineEntity = (
|
const processTimelineEntity = (
|
||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
language: FrontendLocaleData,
|
language: FrontendLocaleData,
|
||||||
states: HassEntity[]
|
entityId: string,
|
||||||
|
states: EntityHistoryState[]
|
||||||
): TimelineEntity => {
|
): TimelineEntity => {
|
||||||
const data: TimelineState[] = [];
|
const data: TimelineState[] = [];
|
||||||
const last_element = states.length - 1;
|
const first: EntityHistoryState = states[0];
|
||||||
|
|
||||||
for (const state of states) {
|
for (const state of states) {
|
||||||
if (data.length > 0 && state.state === data[data.length - 1].state) {
|
if (data.length > 0 && state.s === data[data.length - 1].state) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy the data from the last element as its the newest
|
|
||||||
// and is only needed to localize the data
|
|
||||||
if (!state.entity_id) {
|
|
||||||
state.attributes = states[last_element].attributes;
|
|
||||||
state.entity_id = states[last_element].entity_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
data.push({
|
data.push({
|
||||||
state_localize: computeStateDisplay(localize, state, language),
|
state_localize: computeStateDisplayFromEntityAttributes(
|
||||||
state: state.state,
|
localize,
|
||||||
last_changed: state.last_changed,
|
language,
|
||||||
|
entityId,
|
||||||
|
state.a || first.a,
|
||||||
|
state.s
|
||||||
|
),
|
||||||
|
state: state.s,
|
||||||
|
// lc (last_changed) may be omitted if its the same
|
||||||
|
// as lu (last_updated).
|
||||||
|
last_changed: (state.lc ? state.lc : state.lu) * 1000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: computeStateName(states[0]),
|
name: computeStateNameFromEntityAttributes(entityId, states[0].a),
|
||||||
entity_id: states[0].entity_id,
|
entity_id: entityId,
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const processLineChartEntities = (
|
const processLineChartEntities = (
|
||||||
unit,
|
unit,
|
||||||
entities: HassEntity[][]
|
entities: HistoryStates
|
||||||
): LineChartUnit => {
|
): LineChartUnit => {
|
||||||
const data: LineChartEntity[] = [];
|
const data: LineChartEntity[] = [];
|
||||||
|
|
||||||
for (const states of entities) {
|
Object.keys(entities).forEach((entityId) => {
|
||||||
const last: HassEntity = states[states.length - 1];
|
const states = entities[entityId];
|
||||||
const domain = computeStateDomain(last);
|
const first: EntityHistoryState = states[0];
|
||||||
|
const domain = computeDomain(entityId);
|
||||||
const processedStates: LineChartState[] = [];
|
const processedStates: LineChartState[] = [];
|
||||||
|
|
||||||
for (const state of states) {
|
for (const state of states) {
|
||||||
@@ -259,18 +316,24 @@ const processLineChartEntities = (
|
|||||||
|
|
||||||
if (DOMAINS_USE_LAST_UPDATED.includes(domain)) {
|
if (DOMAINS_USE_LAST_UPDATED.includes(domain)) {
|
||||||
processedState = {
|
processedState = {
|
||||||
state: state.state,
|
state: state.s,
|
||||||
last_changed: state.last_updated,
|
last_changed: state.lu * 1000,
|
||||||
attributes: {},
|
attributes: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const attr of LINE_ATTRIBUTES_TO_KEEP) {
|
for (const attr of LINE_ATTRIBUTES_TO_KEEP) {
|
||||||
if (attr in state.attributes) {
|
if (attr in state.a) {
|
||||||
processedState.attributes![attr] = state.attributes[attr];
|
processedState.attributes![attr] = state.a[attr];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
processedState = state;
|
processedState = {
|
||||||
|
state: state.s,
|
||||||
|
// lc (last_changed) may be omitted if its the same
|
||||||
|
// as lu (last_updated).
|
||||||
|
last_changed: (state.lc ? state.lc : state.lu) * 1000,
|
||||||
|
attributes: {},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -289,52 +352,53 @@ const processLineChartEntities = (
|
|||||||
|
|
||||||
data.push({
|
data.push({
|
||||||
domain,
|
domain,
|
||||||
name: computeStateName(last),
|
name: computeStateNameFromEntityAttributes(entityId, first.a),
|
||||||
entity_id: last.entity_id,
|
entity_id: entityId,
|
||||||
states: processedStates,
|
states: processedStates,
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
unit,
|
unit,
|
||||||
identifier: entities.map((states) => states[0].entity_id).join(""),
|
identifier: Object.keys(entities).join(""),
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateUsesUnits = (state: HassEntity) =>
|
const stateUsesUnits = (state: HassEntity) =>
|
||||||
"unit_of_measurement" in state.attributes ||
|
attributesHaveUnits(state.attributes);
|
||||||
"state_class" in state.attributes;
|
|
||||||
|
const attributesHaveUnits = (attributes: { [key: string]: any }) =>
|
||||||
|
"unit_of_measurement" in attributes || "state_class" in attributes;
|
||||||
|
|
||||||
export const computeHistory = (
|
export const computeHistory = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
stateHistory: HassEntity[][],
|
stateHistory: HistoryStates,
|
||||||
localize: LocalizeFunc
|
localize: LocalizeFunc
|
||||||
): HistoryResult => {
|
): HistoryResult => {
|
||||||
const lineChartDevices: { [unit: string]: HassEntity[][] } = {};
|
const lineChartDevices: { [unit: string]: HistoryStates } = {};
|
||||||
const timelineDevices: TimelineEntity[] = [];
|
const timelineDevices: TimelineEntity[] = [];
|
||||||
if (!stateHistory) {
|
if (!stateHistory) {
|
||||||
return { line: [], timeline: [] };
|
return { line: [], timeline: [] };
|
||||||
}
|
}
|
||||||
|
Object.keys(stateHistory).forEach((entityId) => {
|
||||||
stateHistory.forEach((stateInfo) => {
|
const stateInfo = stateHistory[entityId];
|
||||||
if (stateInfo.length === 0) {
|
if (stateInfo.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entityId = stateInfo[0].entity_id;
|
|
||||||
const currentState =
|
const currentState =
|
||||||
entityId in hass.states ? hass.states[entityId] : undefined;
|
entityId in hass.states ? hass.states[entityId] : undefined;
|
||||||
const stateWithUnitorStateClass =
|
const stateWithUnitorStateClass =
|
||||||
!currentState &&
|
!currentState &&
|
||||||
stateInfo.find((state) => state.attributes && stateUsesUnits(state));
|
stateInfo.find((state) => state.a && attributesHaveUnits(state.a));
|
||||||
|
|
||||||
let unit: string | undefined;
|
let unit: string | undefined;
|
||||||
|
|
||||||
if (currentState && stateUsesUnits(currentState)) {
|
if (currentState && stateUsesUnits(currentState)) {
|
||||||
unit = currentState.attributes.unit_of_measurement || " ";
|
unit = currentState.attributes.unit_of_measurement || " ";
|
||||||
} else if (stateWithUnitorStateClass) {
|
} else if (stateWithUnitorStateClass) {
|
||||||
unit = stateWithUnitorStateClass.attributes.unit_of_measurement || " ";
|
unit = stateWithUnitorStateClass.a.unit_of_measurement || " ";
|
||||||
} else {
|
} else {
|
||||||
unit = {
|
unit = {
|
||||||
climate: hass.config.unit_system.temperature,
|
climate: hass.config.unit_system.temperature,
|
||||||
@@ -348,12 +412,15 @@ export const computeHistory = (
|
|||||||
|
|
||||||
if (!unit) {
|
if (!unit) {
|
||||||
timelineDevices.push(
|
timelineDevices.push(
|
||||||
processTimelineEntity(localize, hass.locale, stateInfo)
|
processTimelineEntity(localize, hass.locale, entityId, stateInfo)
|
||||||
);
|
);
|
||||||
} else if (unit in lineChartDevices) {
|
} else if (unit in lineChartDevices && entityId in lineChartDevices[unit]) {
|
||||||
lineChartDevices[unit].push(stateInfo);
|
lineChartDevices[unit][entityId].push(...stateInfo);
|
||||||
} else {
|
} else {
|
||||||
lineChartDevices[unit] = [stateInfo];
|
if (!(unit in lineChartDevices)) {
|
||||||
|
lineChartDevices[unit] = {};
|
||||||
|
}
|
||||||
|
lineChartDevices[unit][entityId] = stateInfo;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -42,8 +42,18 @@ export const domainToName = (
|
|||||||
manifest?: IntegrationManifest
|
manifest?: IntegrationManifest
|
||||||
) => localize(`component.${domain}.title`) || manifest?.name || domain;
|
) => localize(`component.${domain}.title`) || manifest?.name || domain;
|
||||||
|
|
||||||
export const fetchIntegrationManifests = (hass: HomeAssistant) =>
|
export const fetchIntegrationManifests = (
|
||||||
hass.callWS<IntegrationManifest[]>({ type: "manifest/list" });
|
hass: HomeAssistant,
|
||||||
|
integrations?: string[]
|
||||||
|
) => {
|
||||||
|
const params: any = {
|
||||||
|
type: "manifest/list",
|
||||||
|
};
|
||||||
|
if (integrations) {
|
||||||
|
params.integrations = integrations;
|
||||||
|
}
|
||||||
|
return hass.callWS<IntegrationManifest[]>(params);
|
||||||
|
};
|
||||||
|
|
||||||
export const fetchIntegrationManifest = (
|
export const fetchIntegrationManifest = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
@@ -1,5 +1,9 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import { BINARY_STATE_OFF, BINARY_STATE_ON } from "../common/const";
|
import {
|
||||||
|
BINARY_STATE_OFF,
|
||||||
|
BINARY_STATE_ON,
|
||||||
|
DOMAINS_WITH_DYNAMIC_PICTURE,
|
||||||
|
} from "../common/const";
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||||
import { LocalizeFunc } from "../common/translations/localize";
|
import { LocalizeFunc } from "../common/translations/localize";
|
||||||
@@ -9,26 +13,51 @@ import { UNAVAILABLE_STATES } from "./entity";
|
|||||||
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
|
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
|
||||||
export const CONTINUOUS_DOMAINS = ["proximity", "sensor"];
|
export const CONTINUOUS_DOMAINS = ["proximity", "sensor"];
|
||||||
|
|
||||||
|
export interface LogbookStreamMessage {
|
||||||
|
events: LogbookEntry[];
|
||||||
|
start_time?: number; // Start time of this historical chunk
|
||||||
|
end_time?: number; // End time of this historical chunk
|
||||||
|
partial?: boolean; // Indiciates more historical chunks are coming
|
||||||
|
}
|
||||||
|
|
||||||
export interface LogbookEntry {
|
export interface LogbookEntry {
|
||||||
when: number;
|
// Base data
|
||||||
|
when: number; // Python timestamp. Do *1000 to get JS timestamp.
|
||||||
name: string;
|
name: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
entity_id?: string;
|
entity_id?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
source?: string;
|
source?: string; // The trigger source
|
||||||
domain?: string;
|
domain?: string;
|
||||||
|
state?: string; // The state of the entity
|
||||||
|
// Context data
|
||||||
context_id?: string;
|
context_id?: string;
|
||||||
context_user_id?: string;
|
context_user_id?: string;
|
||||||
context_event_type?: string;
|
context_event_type?: string;
|
||||||
context_domain?: string;
|
context_domain?: string;
|
||||||
context_service?: string;
|
context_service?: string; // Service calls only
|
||||||
context_entity_id?: string;
|
context_entity_id?: string;
|
||||||
context_entity_id_name?: string;
|
context_entity_id_name?: string; // Legacy, not longer sent
|
||||||
context_name?: string;
|
context_name?: string;
|
||||||
|
context_state?: string; // The state of the entity
|
||||||
|
context_source?: string; // The trigger source
|
||||||
context_message?: string;
|
context_message?: string;
|
||||||
state?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Localization mapping for all the triggers in core
|
||||||
|
// in homeassistant.components.homeassistant.triggers
|
||||||
|
//
|
||||||
|
const triggerPhrases = {
|
||||||
|
"numeric state of": "triggered_by_numeric_state_of", // number state trigger
|
||||||
|
"state of": "triggered_by_state_of", // state trigger
|
||||||
|
event: "triggered_by_event", // event trigger
|
||||||
|
time: "triggered_by_time", // time trigger
|
||||||
|
"time pattern": "triggered_by_time_pattern", // time trigger
|
||||||
|
"Home Assistant stopping": "triggered_by_homeassistant_stopping", // stop event
|
||||||
|
"Home Assistant starting": "triggered_by_homeassistant_starting", // start event
|
||||||
|
};
|
||||||
|
|
||||||
const DATA_CACHE: {
|
const DATA_CACHE: {
|
||||||
[cacheKey: string]: { [entityId: string]: Promise<LogbookEntry[]> };
|
[cacheKey: string]: { [entityId: string]: Promise<LogbookEntry[]> };
|
||||||
} = {};
|
} = {};
|
||||||
@@ -38,17 +67,13 @@ export const getLogbookDataForContext = async (
|
|||||||
startDate: string,
|
startDate: string,
|
||||||
contextId?: string
|
contextId?: string
|
||||||
): Promise<LogbookEntry[]> => {
|
): Promise<LogbookEntry[]> => {
|
||||||
const localize = await hass.loadBackendTranslation("device_class");
|
await hass.loadBackendTranslation("device_class");
|
||||||
return addLogbookMessage(
|
return getLogbookDataFromServer(
|
||||||
hass,
|
hass,
|
||||||
localize,
|
startDate,
|
||||||
await getLogbookDataFromServer(
|
undefined,
|
||||||
hass,
|
undefined,
|
||||||
startDate,
|
contextId
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
contextId
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,99 +81,173 @@ export const getLogbookData = async (
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
startDate: string,
|
startDate: string,
|
||||||
endDate: string,
|
endDate: string,
|
||||||
entityId?: string
|
entityIds?: string[],
|
||||||
|
deviceIds?: string[]
|
||||||
): Promise<LogbookEntry[]> => {
|
): Promise<LogbookEntry[]> => {
|
||||||
const localize = await hass.loadBackendTranslation("device_class");
|
await hass.loadBackendTranslation("device_class");
|
||||||
return addLogbookMessage(
|
return deviceIds?.length
|
||||||
hass,
|
? getLogbookDataFromServer(
|
||||||
localize,
|
|
||||||
await getLogbookDataCache(hass, startDate, endDate, entityId)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addLogbookMessage = (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
localize: LocalizeFunc,
|
|
||||||
logbookData: LogbookEntry[]
|
|
||||||
): LogbookEntry[] => {
|
|
||||||
for (const entry of logbookData) {
|
|
||||||
const stateObj = hass!.states[entry.entity_id!];
|
|
||||||
if (entry.state && stateObj) {
|
|
||||||
entry.message = getLogbookMessage(
|
|
||||||
hass,
|
hass,
|
||||||
localize,
|
startDate,
|
||||||
entry.state,
|
endDate,
|
||||||
stateObj,
|
entityIds,
|
||||||
computeDomain(entry.entity_id!)
|
undefined,
|
||||||
);
|
deviceIds
|
||||||
}
|
)
|
||||||
}
|
: getLogbookDataCache(hass, startDate, endDate, entityIds);
|
||||||
return logbookData;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLogbookDataCache = async (
|
const getLogbookDataCache = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
startDate: string,
|
startDate: string,
|
||||||
endDate: string,
|
endDate: string,
|
||||||
entityId?: string
|
entityId?: string[]
|
||||||
) => {
|
) => {
|
||||||
const ALL_ENTITIES = "*";
|
const ALL_ENTITIES = "*";
|
||||||
|
|
||||||
if (!entityId) {
|
const entityIdKey = entityId ? entityId.toString() : ALL_ENTITIES;
|
||||||
entityId = ALL_ENTITIES;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cacheKey = `${startDate}${endDate}`;
|
const cacheKey = `${startDate}${endDate}`;
|
||||||
|
|
||||||
if (!DATA_CACHE[cacheKey]) {
|
if (!DATA_CACHE[cacheKey]) {
|
||||||
DATA_CACHE[cacheKey] = {};
|
DATA_CACHE[cacheKey] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entityId in DATA_CACHE[cacheKey]) {
|
if (entityIdKey in DATA_CACHE[cacheKey]) {
|
||||||
return DATA_CACHE[cacheKey][entityId];
|
return DATA_CACHE[cacheKey][entityIdKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entityId !== ALL_ENTITIES && DATA_CACHE[cacheKey][ALL_ENTITIES]) {
|
if (entityId && DATA_CACHE[cacheKey][ALL_ENTITIES]) {
|
||||||
const entities = await DATA_CACHE[cacheKey][ALL_ENTITIES];
|
const entities = await DATA_CACHE[cacheKey][ALL_ENTITIES];
|
||||||
return entities.filter((entity) => entity.entity_id === entityId);
|
return entities.filter(
|
||||||
|
(entity) => entity.entity_id && entityId.includes(entity.entity_id)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
DATA_CACHE[cacheKey][entityId] = getLogbookDataFromServer(
|
DATA_CACHE[cacheKey][entityIdKey] = getLogbookDataFromServer(
|
||||||
hass,
|
hass,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
entityId !== ALL_ENTITIES ? entityId : undefined
|
entityId
|
||||||
).then((entries) => entries.reverse());
|
);
|
||||||
return DATA_CACHE[cacheKey][entityId];
|
return DATA_CACHE[cacheKey][entityIdKey];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLogbookDataFromServer = (
|
const getLogbookDataFromServer = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
startDate: string,
|
startDate: string,
|
||||||
endDate?: string,
|
endDate?: string,
|
||||||
entityId?: string,
|
entityIds?: string[],
|
||||||
contextId?: string
|
contextId?: string,
|
||||||
) => {
|
deviceIds?: string[]
|
||||||
let params: any = {
|
): Promise<LogbookEntry[]> => {
|
||||||
|
// If all specified filters are empty lists, we can return an empty list.
|
||||||
|
if (
|
||||||
|
(entityIds || deviceIds) &&
|
||||||
|
(!entityIds || entityIds.length === 0) &&
|
||||||
|
(!deviceIds || deviceIds.length === 0)
|
||||||
|
) {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const params: any = {
|
||||||
type: "logbook/get_events",
|
type: "logbook/get_events",
|
||||||
start_time: startDate,
|
start_time: startDate,
|
||||||
};
|
};
|
||||||
if (endDate) {
|
if (endDate) {
|
||||||
params = { ...params, end_time: endDate };
|
params.end_time = endDate;
|
||||||
}
|
}
|
||||||
if (entityId) {
|
if (entityIds?.length) {
|
||||||
params = { ...params, entity_ids: entityId.split(",") };
|
params.entity_ids = entityIds;
|
||||||
} else if (contextId) {
|
}
|
||||||
params = { ...params, context_id: contextId };
|
if (deviceIds?.length) {
|
||||||
|
params.device_ids = deviceIds;
|
||||||
|
}
|
||||||
|
if (contextId) {
|
||||||
|
params.context_id = contextId;
|
||||||
}
|
}
|
||||||
return hass.callWS<LogbookEntry[]>(params);
|
return hass.callWS<LogbookEntry[]>(params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const subscribeLogbook = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
callbackFunction: (message: LogbookStreamMessage) => void,
|
||||||
|
startDate: string,
|
||||||
|
endDate: string,
|
||||||
|
entityIds?: string[],
|
||||||
|
deviceIds?: string[]
|
||||||
|
): Promise<UnsubscribeFunc> => {
|
||||||
|
// If all specified filters are empty lists, we can return an empty list.
|
||||||
|
if (
|
||||||
|
(entityIds || deviceIds) &&
|
||||||
|
(!entityIds || entityIds.length === 0) &&
|
||||||
|
(!deviceIds || deviceIds.length === 0)
|
||||||
|
) {
|
||||||
|
return Promise.reject("No entities or devices");
|
||||||
|
}
|
||||||
|
const params: any = {
|
||||||
|
type: "logbook/event_stream",
|
||||||
|
start_time: startDate,
|
||||||
|
end_time: endDate,
|
||||||
|
};
|
||||||
|
if (entityIds?.length) {
|
||||||
|
params.entity_ids = entityIds;
|
||||||
|
}
|
||||||
|
if (deviceIds?.length) {
|
||||||
|
params.device_ids = deviceIds;
|
||||||
|
}
|
||||||
|
return hass.connection.subscribeMessage<LogbookStreamMessage>(
|
||||||
|
(message) => callbackFunction(message),
|
||||||
|
params
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const clearLogbookCache = (startDate: string, endDate: string) => {
|
export const clearLogbookCache = (startDate: string, endDate: string) => {
|
||||||
DATA_CACHE[`${startDate}${endDate}`] = {};
|
DATA_CACHE[`${startDate}${endDate}`] = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLogbookMessage = (
|
export const createHistoricState = (
|
||||||
|
currentStateObj: HassEntity,
|
||||||
|
state?: string
|
||||||
|
): HassEntity => <HassEntity>(<unknown>{
|
||||||
|
entity_id: currentStateObj.entity_id,
|
||||||
|
state: state,
|
||||||
|
attributes: {
|
||||||
|
// Rebuild the historical state by copying static attributes only
|
||||||
|
device_class: currentStateObj?.attributes.device_class,
|
||||||
|
source_type: currentStateObj?.attributes.source_type,
|
||||||
|
has_date: currentStateObj?.attributes.has_date,
|
||||||
|
has_time: currentStateObj?.attributes.has_time,
|
||||||
|
// We do not want to use dynamic entity pictures (e.g., from media player) for the log book rendering,
|
||||||
|
// as they would present a false state in the log (played media right now vs actual historic data).
|
||||||
|
entity_picture_local: DOMAINS_WITH_DYNAMIC_PICTURE.has(
|
||||||
|
computeDomain(currentStateObj.entity_id)
|
||||||
|
)
|
||||||
|
? undefined
|
||||||
|
: currentStateObj?.attributes.entity_picture_local,
|
||||||
|
entity_picture: DOMAINS_WITH_DYNAMIC_PICTURE.has(
|
||||||
|
computeDomain(currentStateObj.entity_id)
|
||||||
|
)
|
||||||
|
? undefined
|
||||||
|
: currentStateObj?.attributes.entity_picture,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const localizeTriggerSource = (
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
source: string
|
||||||
|
) => {
|
||||||
|
for (const triggerPhrase in triggerPhrases) {
|
||||||
|
if (source.startsWith(triggerPhrase)) {
|
||||||
|
return source.replace(
|
||||||
|
triggerPhrase,
|
||||||
|
`${localize(`ui.components.logbook.${triggerPhrases[triggerPhrase]}`)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return source;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const localizeStateMessage = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
state: string,
|
state: string,
|
||||||
|
@@ -131,9 +131,9 @@ export interface CallServiceActionConfig extends BaseActionConfig {
|
|||||||
action: "call-service";
|
action: "call-service";
|
||||||
service: string;
|
service: string;
|
||||||
target?: HassServiceTarget;
|
target?: HassServiceTarget;
|
||||||
service_data?: {
|
// "service_data" is kept for backwards compatibility. Replaced by "data".
|
||||||
[key: string]: any;
|
service_data?: Record<string, unknown>;
|
||||||
};
|
data?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NavigateActionConfig extends BaseActionConfig {
|
export interface NavigateActionConfig extends BaseActionConfig {
|
||||||
|
@@ -47,12 +47,17 @@ export interface SceneConfig {
|
|||||||
name: string;
|
name: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
entities: SceneEntities;
|
entities: SceneEntities;
|
||||||
|
metadata?: SceneMetaData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SceneEntities {
|
export interface SceneEntities {
|
||||||
[entityId: string]: string | { state: string; [key: string]: any };
|
[entityId: string]: string | { state: string; [key: string]: any };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SceneMetaData {
|
||||||
|
[entityId: string]: { entity_only?: boolean | undefined };
|
||||||
|
}
|
||||||
|
|
||||||
export const activateScene = (
|
export const activateScene = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityId: string
|
entityId: string
|
||||||
|
@@ -52,7 +52,7 @@ export const getHassTranslations = async (
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
language: string,
|
language: string,
|
||||||
category: TranslationCategory,
|
category: TranslationCategory,
|
||||||
integration?: string,
|
integration?: string | string[],
|
||||||
config_flow?: boolean
|
config_flow?: boolean
|
||||||
): Promise<Record<string, unknown>> => {
|
): Promise<Record<string, unknown>> => {
|
||||||
const result = await hass.callWS<{ resources: Record<string, unknown> }>({
|
const result = await hass.callWS<{ resources: Record<string, unknown> }>({
|
||||||
|
@@ -7,7 +7,10 @@ import type {
|
|||||||
import { BINARY_STATE_ON } from "../common/const";
|
import { BINARY_STATE_ON } from "../common/const";
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||||
import { supportsFeature } from "../common/entity/supports-feature";
|
import {
|
||||||
|
supportsFeature,
|
||||||
|
supportsFeatureFromAttributes,
|
||||||
|
} from "../common/entity/supports-feature";
|
||||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
@@ -35,8 +38,13 @@ export interface UpdateEntity extends HassEntityBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const updateUsesProgress = (entity: UpdateEntity): boolean =>
|
export const updateUsesProgress = (entity: UpdateEntity): boolean =>
|
||||||
supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) &&
|
updateUsesProgressFromAttributes(entity.attributes);
|
||||||
typeof entity.attributes.in_progress === "number";
|
|
||||||
|
export const updateUsesProgressFromAttributes = (attributes: {
|
||||||
|
[key: string]: any;
|
||||||
|
}): boolean =>
|
||||||
|
supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS) &&
|
||||||
|
typeof attributes.in_progress === "number";
|
||||||
|
|
||||||
export const updateCanInstall = (
|
export const updateCanInstall = (
|
||||||
entity: UpdateEntity,
|
entity: UpdateEntity,
|
||||||
@@ -49,6 +57,11 @@ export const updateCanInstall = (
|
|||||||
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
|
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
|
||||||
updateUsesProgress(entity) || !!entity.attributes.in_progress;
|
updateUsesProgress(entity) || !!entity.attributes.in_progress;
|
||||||
|
|
||||||
|
export const updateIsInstallingFromAttributes = (attributes: {
|
||||||
|
[key: string]: any;
|
||||||
|
}): boolean =>
|
||||||
|
updateUsesProgressFromAttributes(attributes) || !!attributes.in_progress;
|
||||||
|
|
||||||
export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
|
export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
|
||||||
hass.callWS<string | null>({
|
hass.callWS<string | null>({
|
||||||
type: "update/release_notes",
|
type: "update/release_notes",
|
||||||
|
@@ -2,9 +2,21 @@ import {
|
|||||||
mdiAlertCircleOutline,
|
mdiAlertCircleOutline,
|
||||||
mdiGauge,
|
mdiGauge,
|
||||||
mdiWaterPercent,
|
mdiWaterPercent,
|
||||||
|
mdiWeatherCloudy,
|
||||||
mdiWeatherFog,
|
mdiWeatherFog,
|
||||||
|
mdiWeatherHail,
|
||||||
|
mdiWeatherLightning,
|
||||||
|
mdiWeatherLightningRainy,
|
||||||
|
mdiWeatherNight,
|
||||||
|
mdiWeatherNightPartlyCloudy,
|
||||||
|
mdiWeatherPartlyCloudy,
|
||||||
|
mdiWeatherPouring,
|
||||||
mdiWeatherRainy,
|
mdiWeatherRainy,
|
||||||
|
mdiWeatherSnowy,
|
||||||
|
mdiWeatherSnowyRainy,
|
||||||
|
mdiWeatherSunny,
|
||||||
mdiWeatherWindy,
|
mdiWeatherWindy,
|
||||||
|
mdiWeatherWindyVariant,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import {
|
import {
|
||||||
HassEntityAttributeBase,
|
HassEntityAttributeBase,
|
||||||
@@ -57,7 +69,21 @@ export const weatherSVGs = new Set<string>([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export const weatherIcons = {
|
export const weatherIcons = {
|
||||||
|
"clear-night": mdiWeatherNight,
|
||||||
|
cloudy: mdiWeatherCloudy,
|
||||||
exceptional: mdiAlertCircleOutline,
|
exceptional: mdiAlertCircleOutline,
|
||||||
|
fog: mdiWeatherFog,
|
||||||
|
hail: mdiWeatherHail,
|
||||||
|
lightning: mdiWeatherLightning,
|
||||||
|
"lightning-rainy": mdiWeatherLightningRainy,
|
||||||
|
partlycloudy: mdiWeatherPartlyCloudy,
|
||||||
|
pouring: mdiWeatherPouring,
|
||||||
|
rainy: mdiWeatherRainy,
|
||||||
|
snowy: mdiWeatherSnowy,
|
||||||
|
"snowy-rainy": mdiWeatherSnowyRainy,
|
||||||
|
sunny: mdiWeatherSunny,
|
||||||
|
windy: mdiWeatherWindy,
|
||||||
|
"windy-variant": mdiWeatherWindyVariant,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const weatherAttrIcons = {
|
export const weatherAttrIcons = {
|
||||||
@@ -437,6 +463,13 @@ export const getWeatherStateIcon = (
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const weatherIcon = (state?: string, nightTime?: boolean): string =>
|
||||||
|
!state
|
||||||
|
? undefined
|
||||||
|
: nightTime && state === "partlycloudy"
|
||||||
|
? mdiWeatherNightPartlyCloudy
|
||||||
|
: weatherIcons[state];
|
||||||
|
|
||||||
const DAY_IN_MILLISECONDS = 86400000;
|
const DAY_IN_MILLISECONDS = 86400000;
|
||||||
|
|
||||||
export const isForecastHourly = (
|
export const isForecastHourly = (
|
||||||
|
@@ -145,7 +145,7 @@ export interface ZWaveJSController {
|
|||||||
supports_timers: boolean;
|
supports_timers: boolean;
|
||||||
is_heal_network_active: boolean;
|
is_heal_network_active: boolean;
|
||||||
inclusion_state: InclusionState;
|
inclusion_state: InclusionState;
|
||||||
nodes: number[];
|
nodes: ZWaveJSNodeStatus[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ZWaveJSNodeStatus {
|
export interface ZWaveJSNodeStatus {
|
||||||
@@ -167,6 +167,9 @@ export interface ZwaveJSNodeMetadata {
|
|||||||
wakeup: string;
|
wakeup: string;
|
||||||
reset: string;
|
reset: string;
|
||||||
device_database_url: string;
|
device_database_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ZwaveJSNodeComments {
|
||||||
comments: ZWaveJSNodeComment[];
|
comments: ZWaveJSNodeComment[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,8 +203,7 @@ export interface ZWaveJSNodeConfigParamMetadata {
|
|||||||
|
|
||||||
export interface ZWaveJSSetConfigParamData {
|
export interface ZWaveJSSetConfigParamData {
|
||||||
type: string;
|
type: string;
|
||||||
entry_id: string;
|
device_id: string;
|
||||||
node_id: number;
|
|
||||||
property: number;
|
property: number;
|
||||||
property_key?: number;
|
property_key?: number;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
@@ -228,6 +230,20 @@ export interface ZWaveJSHealNetworkStatusMessage {
|
|||||||
heal_node_status: { [key: number]: string };
|
heal_node_status: { [key: number]: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ZWaveJSControllerStatisticsUpdatedMessage {
|
||||||
|
event: "statistics updated";
|
||||||
|
source: "controller";
|
||||||
|
messages_tx: number;
|
||||||
|
messages_rx: number;
|
||||||
|
messages_dropped_tx: number;
|
||||||
|
messages_dropped_rx: number;
|
||||||
|
nak: number;
|
||||||
|
can: number;
|
||||||
|
timeout_ack: number;
|
||||||
|
timeout_response: number;
|
||||||
|
timeout_callback: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ZWaveJSRemovedNode {
|
export interface ZWaveJSRemovedNode {
|
||||||
node_id: number;
|
node_id: number;
|
||||||
manufacturer: string;
|
manufacturer: string;
|
||||||
@@ -285,12 +301,23 @@ export const migrateZwave = (
|
|||||||
|
|
||||||
export const fetchZwaveNetworkStatus = (
|
export const fetchZwaveNetworkStatus = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry_id: string
|
device_or_entry_id: {
|
||||||
): Promise<ZWaveJSNetwork> =>
|
device_id?: string;
|
||||||
hass.callWS({
|
entry_id?: string;
|
||||||
|
}
|
||||||
|
): Promise<ZWaveJSNetwork> => {
|
||||||
|
if (device_or_entry_id.device_id && device_or_entry_id.entry_id) {
|
||||||
|
throw new Error("Only one of device or entry ID should be supplied.");
|
||||||
|
}
|
||||||
|
if (!device_or_entry_id.device_id && !device_or_entry_id.entry_id) {
|
||||||
|
throw new Error("Either device or entry ID should be supplied.");
|
||||||
|
}
|
||||||
|
return hass.callWS({
|
||||||
type: "zwave_js/network_status",
|
type: "zwave_js/network_status",
|
||||||
entry_id,
|
device_id: device_or_entry_id.device_id,
|
||||||
|
entry_id: device_or_entry_id.entry_id,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const fetchZwaveDataCollectionStatus = (
|
export const fetchZwaveDataCollectionStatus = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -427,49 +454,50 @@ export const unprovisionZwaveSmartStartNode = (
|
|||||||
|
|
||||||
export const fetchZwaveNodeStatus = (
|
export const fetchZwaveNodeStatus = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry_id: string,
|
device_id: string
|
||||||
node_id: number
|
|
||||||
): Promise<ZWaveJSNodeStatus> =>
|
): Promise<ZWaveJSNodeStatus> =>
|
||||||
hass.callWS({
|
hass.callWS({
|
||||||
type: "zwave_js/node_status",
|
type: "zwave_js/node_status",
|
||||||
entry_id,
|
device_id,
|
||||||
node_id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fetchZwaveNodeMetadata = (
|
export const fetchZwaveNodeMetadata = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry_id: string,
|
device_id: string
|
||||||
node_id: number
|
|
||||||
): Promise<ZwaveJSNodeMetadata> =>
|
): Promise<ZwaveJSNodeMetadata> =>
|
||||||
hass.callWS({
|
hass.callWS({
|
||||||
type: "zwave_js/node_metadata",
|
type: "zwave_js/node_metadata",
|
||||||
entry_id,
|
device_id,
|
||||||
node_id,
|
});
|
||||||
|
|
||||||
|
export const fetchZwaveNodeComments = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
device_id: string
|
||||||
|
): Promise<ZwaveJSNodeComments> =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "zwave_js/node_comments",
|
||||||
|
device_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fetchZwaveNodeConfigParameters = (
|
export const fetchZwaveNodeConfigParameters = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry_id: string,
|
device_id: string
|
||||||
node_id: number
|
|
||||||
): Promise<ZWaveJSNodeConfigParams> =>
|
): Promise<ZWaveJSNodeConfigParams> =>
|
||||||
hass.callWS({
|
hass.callWS({
|
||||||
type: "zwave_js/get_config_parameters",
|
type: "zwave_js/get_config_parameters",
|
||||||
entry_id,
|
device_id,
|
||||||
node_id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setZwaveNodeConfigParameter = (
|
export const setZwaveNodeConfigParameter = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry_id: string,
|
device_id: string,
|
||||||
node_id: number,
|
|
||||||
property: number,
|
property: number,
|
||||||
value: number,
|
value: number,
|
||||||
property_key?: number
|
property_key?: number
|
||||||
): Promise<ZWaveJSSetConfigParamResult> => {
|
): Promise<ZWaveJSSetConfigParamResult> => {
|
||||||
const data: ZWaveJSSetConfigParamData = {
|
const data: ZWaveJSSetConfigParamData = {
|
||||||
type: "zwave_js/set_config_parameter",
|
type: "zwave_js/set_config_parameter",
|
||||||
entry_id,
|
device_id,
|
||||||
node_id,
|
|
||||||
property,
|
property,
|
||||||
value,
|
value,
|
||||||
property_key,
|
property_key,
|
||||||
@@ -479,42 +507,36 @@ export const setZwaveNodeConfigParameter = (
|
|||||||
|
|
||||||
export const reinterviewZwaveNode = (
|
export const reinterviewZwaveNode = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry_id: string,
|
device_id: string,
|
||||||
node_id: number,
|
|
||||||
callbackFunction: (message: ZWaveJSRefreshNodeStatusMessage) => void
|
callbackFunction: (message: ZWaveJSRefreshNodeStatusMessage) => void
|
||||||
): Promise<UnsubscribeFunc> =>
|
): Promise<UnsubscribeFunc> =>
|
||||||
hass.connection.subscribeMessage(
|
hass.connection.subscribeMessage(
|
||||||
(message: any) => callbackFunction(message),
|
(message: any) => callbackFunction(message),
|
||||||
{
|
{
|
||||||
type: "zwave_js/refresh_node_info",
|
type: "zwave_js/refresh_node_info",
|
||||||
entry_id,
|
device_id,
|
||||||
node_id,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const healZwaveNode = (
|
export const healZwaveNode = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry_id: string,
|
device_id: string
|
||||||
node_id: number
|
|
||||||
): Promise<boolean> =>
|
): Promise<boolean> =>
|
||||||
hass.callWS({
|
hass.callWS({
|
||||||
type: "zwave_js/heal_node",
|
type: "zwave_js/heal_node",
|
||||||
entry_id,
|
device_id,
|
||||||
node_id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const removeFailedZwaveNode = (
|
export const removeFailedZwaveNode = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry_id: string,
|
device_id: string,
|
||||||
node_id: number,
|
|
||||||
callbackFunction: (message: any) => void
|
callbackFunction: (message: any) => void
|
||||||
): Promise<UnsubscribeFunc> =>
|
): Promise<UnsubscribeFunc> =>
|
||||||
hass.connection.subscribeMessage(
|
hass.connection.subscribeMessage(
|
||||||
(message: any) => callbackFunction(message),
|
(message: any) => callbackFunction(message),
|
||||||
{
|
{
|
||||||
type: "zwave_js/remove_failed_node",
|
type: "zwave_js/remove_failed_node",
|
||||||
entry_id,
|
device_id,
|
||||||
node_id,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -538,16 +560,14 @@ export const stopHealZwaveNetwork = (
|
|||||||
|
|
||||||
export const subscribeZwaveNodeReady = (
|
export const subscribeZwaveNodeReady = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry_id: string,
|
device_id: string,
|
||||||
node_id: number,
|
|
||||||
callbackFunction: (message) => void
|
callbackFunction: (message) => void
|
||||||
): Promise<UnsubscribeFunc> =>
|
): Promise<UnsubscribeFunc> =>
|
||||||
hass.connection.subscribeMessage(
|
hass.connection.subscribeMessage(
|
||||||
(message: any) => callbackFunction(message),
|
(message: any) => callbackFunction(message),
|
||||||
{
|
{
|
||||||
type: "zwave_js/node_ready",
|
type: "zwave_js/node_ready",
|
||||||
entry_id,
|
device_id,
|
||||||
node_id,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -564,6 +584,19 @@ export const subscribeHealZwaveNetworkProgress = (
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const subscribeZwaveControllerStatistics = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry_id: string,
|
||||||
|
callbackFunction: (message: ZWaveJSControllerStatisticsUpdatedMessage) => void
|
||||||
|
): Promise<UnsubscribeFunc> =>
|
||||||
|
hass.connection.subscribeMessage(
|
||||||
|
(message: any) => callbackFunction(message),
|
||||||
|
{
|
||||||
|
type: "zwave_js/subscribe_controller_statistics",
|
||||||
|
entry_id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const getZwaveJsIdentifiersFromDevice = (
|
export const getZwaveJsIdentifiersFromDevice = (
|
||||||
device: DeviceRegistryEntry
|
device: DeviceRegistryEntry
|
||||||
): ZWaveJSNodeIdentifiers | undefined => {
|
): ZWaveJSNodeIdentifiers | undefined => {
|
||||||
|
@@ -309,7 +309,7 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
: this._step.type === "abort"
|
: this._step.type === "abort"
|
||||||
? html`
|
? html`
|
||||||
<step-flow-abort
|
<step-flow-abort
|
||||||
.flowConfig=${this._params.flowConfig}
|
.params=${this._params}
|
||||||
.step=${this._step}
|
.step=${this._step}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.domain=${this._step.handler}
|
.domain=${this._step.handler}
|
||||||
|
@@ -131,6 +131,7 @@ export interface DataEntryFlowDialogParams {
|
|||||||
}) => void;
|
}) => void;
|
||||||
flowConfig: FlowConfig;
|
flowConfig: FlowConfig;
|
||||||
showAdvanced?: boolean;
|
showAdvanced?: boolean;
|
||||||
|
dialogParentElement?: HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadDataEntryFlowDialog = () => import("./dialog-data-entry-flow");
|
export const loadDataEntryFlowDialog = () => import("./dialog-data-entry-flow");
|
||||||
@@ -146,6 +147,7 @@ export const showFlowDialog = (
|
|||||||
dialogParams: {
|
dialogParams: {
|
||||||
...dialogParams,
|
...dialogParams,
|
||||||
flowConfig,
|
flowConfig,
|
||||||
|
dialogParentElement: element,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -1,15 +1,25 @@
|
|||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import {
|
||||||
|
CSSResultGroup,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
TemplateResult,
|
||||||
|
PropertyValues,
|
||||||
|
} from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { DataEntryFlowStepAbort } from "../../data/data_entry_flow";
|
import { DataEntryFlowStepAbort } from "../../data/data_entry_flow";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import { FlowConfig } from "./show-dialog-data-entry-flow";
|
import { showAddApplicationCredentialDialog } from "../../panels/config/application_credentials/show-dialog-add-application-credential";
|
||||||
import { configFlowContentStyles } from "./styles";
|
import { configFlowContentStyles } from "./styles";
|
||||||
|
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||||
|
import { domainToName } from "../../data/integration";
|
||||||
|
import { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow";
|
||||||
|
import { showConfigFlowDialog } from "./show-dialog-config-flow";
|
||||||
|
|
||||||
@customElement("step-flow-abort")
|
@customElement("step-flow-abort")
|
||||||
class StepFlowAbort extends LitElement {
|
class StepFlowAbort extends LitElement {
|
||||||
@property({ attribute: false }) public flowConfig!: FlowConfig;
|
@property({ attribute: false }) public params!: DataEntryFlowDialogParams;
|
||||||
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@@ -17,11 +27,21 @@ class StepFlowAbort extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public domain!: string;
|
@property({ attribute: false }) public domain!: string;
|
||||||
|
|
||||||
|
protected firstUpdated(changed: PropertyValues) {
|
||||||
|
super.firstUpdated(changed);
|
||||||
|
if (this.step.reason === "missing_credentials") {
|
||||||
|
this._handleMissingCreds();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
|
if (this.step.reason === "missing_credentials") {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
return html`
|
return html`
|
||||||
<h2>${this.hass.localize(`component.${this.domain}.title`)}</h2>
|
<h2>${this.hass.localize(`component.${this.domain}.title`)}</h2>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
${this.flowConfig.renderAbortDescription(this.hass, this.step)}
|
${this.params.flowConfig.renderAbortDescription(this.hass, this.step)}
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<mwc-button @click=${this._flowDone}
|
<mwc-button @click=${this._flowDone}
|
||||||
@@ -33,6 +53,32 @@ class StepFlowAbort extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _handleMissingCreds() {
|
||||||
|
const confirm = await showConfirmationDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.missing_credentials",
|
||||||
|
{
|
||||||
|
integration: domainToName(this.hass.localize, this.domain),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this._flowDone();
|
||||||
|
if (!confirm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Prompt to enter credentials and restart integration setup
|
||||||
|
showAddApplicationCredentialDialog(this.params.dialogParentElement!, {
|
||||||
|
selectedDomain: this.domain,
|
||||||
|
applicationCredentialAddedCallback: () => {
|
||||||
|
showConfigFlowDialog(this.params.dialogParentElement!, {
|
||||||
|
dialogClosedCallback: this.params.dialogClosedCallback,
|
||||||
|
startFlowHandler: this.domain,
|
||||||
|
showAdvanced: this.hass.userData?.showAdvanced,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private _flowDone(): void {
|
private _flowDone(): void {
|
||||||
fireEvent(this, "flow-update", { step: undefined });
|
fireEvent(this, "flow-update", { step: undefined });
|
||||||
}
|
}
|
||||||
|
@@ -86,6 +86,7 @@ export const showDialog = async (
|
|||||||
if (mainWindow.history.state?.replaced) {
|
if (mainWindow.history.state?.replaced) {
|
||||||
LOADED[dialogTag].closedFocusTargets =
|
LOADED[dialogTag].closedFocusTargets =
|
||||||
LOADED[mainWindow.history.state.dialog].closedFocusTargets;
|
LOADED[mainWindow.history.state.dialog].closedFocusTargets;
|
||||||
|
delete LOADED[mainWindow.history.state.dialog].closedFocusTargets;
|
||||||
} else {
|
} else {
|
||||||
LOADED[dialogTag].closedFocusTargets = ancestorsWithProperty(
|
LOADED[dialogTag].closedFocusTargets = ancestorsWithProperty(
|
||||||
deepActiveElement(),
|
deepActiveElement(),
|
||||||
|
@@ -1,35 +1,14 @@
|
|||||||
import {
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
css,
|
|
||||||
CSSResultGroup,
|
|
||||||
html,
|
|
||||||
LitElement,
|
|
||||||
PropertyValues,
|
|
||||||
TemplateResult,
|
|
||||||
} from "lit";
|
|
||||||
import { property, state } from "lit/decorators";
|
import { property, state } from "lit/decorators";
|
||||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
|
||||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
|
||||||
import "../../../components/ha-camera-stream";
|
import "../../../components/ha-camera-stream";
|
||||||
import type { HaCheckbox } from "../../../components/ha-checkbox";
|
import { CameraEntity } from "../../../data/camera";
|
||||||
import "../../../components/ha-checkbox";
|
|
||||||
import {
|
|
||||||
CameraEntity,
|
|
||||||
CameraPreferences,
|
|
||||||
CAMERA_SUPPORT_STREAM,
|
|
||||||
fetchCameraPrefs,
|
|
||||||
STREAM_TYPE_HLS,
|
|
||||||
updateCameraPrefs,
|
|
||||||
} from "../../../data/camera";
|
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import "../../../components/ha-formfield";
|
|
||||||
|
|
||||||
class MoreInfoCamera extends LitElement {
|
class MoreInfoCamera extends LitElement {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public stateObj?: CameraEntity;
|
@property({ attribute: false }) public stateObj?: CameraEntity;
|
||||||
|
|
||||||
@state() private _cameraPrefs?: CameraPreferences;
|
|
||||||
|
|
||||||
@state() private _attached = false;
|
@state() private _attached = false;
|
||||||
|
|
||||||
public connectedCallback() {
|
public connectedCallback() {
|
||||||
@@ -54,83 +33,13 @@ class MoreInfoCamera extends LitElement {
|
|||||||
allow-exoplayer
|
allow-exoplayer
|
||||||
controls
|
controls
|
||||||
></ha-camera-stream>
|
></ha-camera-stream>
|
||||||
${this._cameraPrefs
|
|
||||||
? html`
|
|
||||||
<ha-formfield label="Preload stream">
|
|
||||||
<ha-checkbox
|
|
||||||
.checked=${this._cameraPrefs.preload_stream}
|
|
||||||
@change=${this._handleCheckboxChanged}
|
|
||||||
>
|
|
||||||
</ha-checkbox>
|
|
||||||
</ha-formfield>
|
|
||||||
`
|
|
||||||
: undefined}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues) {
|
|
||||||
if (!changedProps.has("stateObj")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldState = changedProps.get("stateObj") as this["stateObj"];
|
|
||||||
const oldEntityId = oldState ? oldState.entity_id : undefined;
|
|
||||||
const curEntityId = this.stateObj ? this.stateObj.entity_id : undefined;
|
|
||||||
|
|
||||||
// Same entity, ignore.
|
|
||||||
if (curEntityId === oldEntityId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
curEntityId &&
|
|
||||||
isComponentLoaded(this.hass!, "stream") &&
|
|
||||||
supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM) &&
|
|
||||||
// The stream component for HLS streams supports a server-side pre-load
|
|
||||||
// option that client initiated WebRTC streams do not
|
|
||||||
this.stateObj!.attributes.frontend_stream_type === STREAM_TYPE_HLS
|
|
||||||
) {
|
|
||||||
// Fetch in background while we set up the video.
|
|
||||||
this._fetchCameraPrefs();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _fetchCameraPrefs() {
|
|
||||||
this._cameraPrefs = await fetchCameraPrefs(
|
|
||||||
this.hass!,
|
|
||||||
this.stateObj!.entity_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _handleCheckboxChanged(ev) {
|
|
||||||
const checkbox = ev.currentTarget as HaCheckbox;
|
|
||||||
try {
|
|
||||||
this._cameraPrefs = await updateCameraPrefs(
|
|
||||||
this.hass!,
|
|
||||||
this.stateObj!.entity_id,
|
|
||||||
{
|
|
||||||
preload_stream: checkbox.checked!,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (err: any) {
|
|
||||||
alert(err.message);
|
|
||||||
checkbox.checked = !checkbox.checked;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
ha-formfield {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
background-color: var(--secondary-background-color);
|
|
||||||
padding-right: 16px;
|
|
||||||
border-bottom-left-radius: 4px;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -1,23 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
mdiAlertCircleOutline,
|
|
||||||
mdiEye,
|
mdiEye,
|
||||||
mdiGauge,
|
mdiGauge,
|
||||||
mdiThermometer,
|
mdiThermometer,
|
||||||
mdiWaterPercent,
|
mdiWaterPercent,
|
||||||
mdiWeatherCloudy,
|
|
||||||
mdiWeatherFog,
|
|
||||||
mdiWeatherHail,
|
|
||||||
mdiWeatherLightning,
|
|
||||||
mdiWeatherLightningRainy,
|
|
||||||
mdiWeatherNight,
|
|
||||||
mdiWeatherPartlyCloudy,
|
|
||||||
mdiWeatherPouring,
|
|
||||||
mdiWeatherRainy,
|
|
||||||
mdiWeatherSnowy,
|
|
||||||
mdiWeatherSnowyRainy,
|
|
||||||
mdiWeatherSunny,
|
|
||||||
mdiWeatherWindy,
|
mdiWeatherWindy,
|
||||||
mdiWeatherWindyVariant,
|
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import {
|
import {
|
||||||
@@ -37,27 +23,10 @@ import {
|
|||||||
getWeatherUnit,
|
getWeatherUnit,
|
||||||
getWind,
|
getWind,
|
||||||
isForecastHourly,
|
isForecastHourly,
|
||||||
|
weatherIcons,
|
||||||
} from "../../../data/weather";
|
} from "../../../data/weather";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
|
|
||||||
const weatherIcons = {
|
|
||||||
"clear-night": mdiWeatherNight,
|
|
||||||
cloudy: mdiWeatherCloudy,
|
|
||||||
exceptional: mdiAlertCircleOutline,
|
|
||||||
fog: mdiWeatherFog,
|
|
||||||
hail: mdiWeatherHail,
|
|
||||||
lightning: mdiWeatherLightning,
|
|
||||||
"lightning-rainy": mdiWeatherLightningRainy,
|
|
||||||
partlycloudy: mdiWeatherPartlyCloudy,
|
|
||||||
pouring: mdiWeatherPouring,
|
|
||||||
rainy: mdiWeatherRainy,
|
|
||||||
snowy: mdiWeatherSnowy,
|
|
||||||
"snowy-rainy": mdiWeatherSnowyRainy,
|
|
||||||
sunny: mdiWeatherSunny,
|
|
||||||
windy: mdiWeatherWindy,
|
|
||||||
"windy-variant": mdiWeatherWindyVariant,
|
|
||||||
};
|
|
||||||
|
|
||||||
@customElement("more-info-weather")
|
@customElement("more-info-weather")
|
||||||
class MoreInfoWeather extends LitElement {
|
class MoreInfoWeather extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -235,6 +204,7 @@ class MoreInfoWeather extends LitElement {
|
|||||||
return css`
|
return css`
|
||||||
ha-svg-icon {
|
ha-svg-icon {
|
||||||
color: var(--paper-item-icon-color);
|
color: var(--paper-item-icon-color);
|
||||||
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
.section {
|
.section {
|
||||||
margin: 16px 0 8px 0;
|
margin: 16px 0 8px 0;
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { startOfYesterday } from "date-fns";
|
import { startOfYesterday } from "date-fns/esm";
|
||||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||||
|
@@ -1,17 +1,11 @@
|
|||||||
import { startOfYesterday } from "date-fns";
|
import { startOfYesterday } from "date-fns/esm";
|
||||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
|
||||||
import { throttle } from "../../common/util/throttle";
|
|
||||||
import "../../components/ha-circular-progress";
|
|
||||||
import { getLogbookData, LogbookEntry } from "../../data/logbook";
|
|
||||||
import { loadTraceContexts, TraceContexts } from "../../data/trace";
|
|
||||||
import { fetchUsers } from "../../data/user";
|
|
||||||
import "../../panels/logbook/ha-logbook";
|
import "../../panels/logbook/ha-logbook";
|
||||||
import { haStyle } from "../../resources/styles";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { HomeAssistant } from "../../types";
|
|
||||||
|
|
||||||
@customElement("ha-more-info-logbook")
|
@customElement("ha-more-info-logbook")
|
||||||
export class MoreInfoLogbook extends LitElement {
|
export class MoreInfoLogbook extends LitElement {
|
||||||
@@ -19,26 +13,14 @@ export class MoreInfoLogbook extends LitElement {
|
|||||||
|
|
||||||
@property() public entityId!: string;
|
@property() public entityId!: string;
|
||||||
|
|
||||||
@state() private _logbookEntries?: LogbookEntry[];
|
|
||||||
|
|
||||||
@state() private _traceContexts?: TraceContexts;
|
|
||||||
|
|
||||||
@state() private _userIdToName = {};
|
|
||||||
|
|
||||||
private _lastLogbookDate?: Date;
|
|
||||||
|
|
||||||
private _fetchUserPromise?: Promise<void>;
|
|
||||||
|
|
||||||
private _error?: string;
|
|
||||||
|
|
||||||
private _showMoreHref = "";
|
private _showMoreHref = "";
|
||||||
|
|
||||||
private _throttleGetLogbookEntries = throttle(() => {
|
private _time = { recent: 86400 };
|
||||||
this._getLogBookData();
|
|
||||||
}, 10000);
|
private _entityIdAsList = memoizeOne((entityId: string) => [entityId]);
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this.entityId) {
|
if (!isComponentLoaded(this.hass, "logbook") || !this.entityId) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
const stateObj = this.hass.states[this.entityId];
|
const stateObj = this.hass.states[this.entityId];
|
||||||
@@ -48,149 +30,34 @@ export class MoreInfoLogbook extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${isComponentLoaded(this.hass, "logbook")
|
<div class="header">
|
||||||
? this._error
|
<div class="title">
|
||||||
? html`<div class="no-entries">
|
${this.hass.localize("ui.dialogs.more_info_control.logbook")}
|
||||||
${`${this.hass.localize(
|
</div>
|
||||||
"ui.components.logbook.retrieval_error"
|
<a href=${this._showMoreHref} @click=${this._close}
|
||||||
)}: ${this._error}`}
|
>${this.hass.localize("ui.dialogs.more_info_control.show_more")}</a
|
||||||
</div>`
|
>
|
||||||
: !this._logbookEntries
|
</div>
|
||||||
? html`
|
<ha-logbook
|
||||||
<ha-circular-progress
|
.hass=${this.hass}
|
||||||
active
|
.time=${this._time}
|
||||||
alt=${this.hass.localize("ui.common.loading")}
|
.entityIds=${this._entityIdAsList(this.entityId)}
|
||||||
></ha-circular-progress>
|
narrow
|
||||||
`
|
no-icon
|
||||||
: this._logbookEntries.length
|
no-name
|
||||||
? html`
|
relative-time
|
||||||
<div class="header">
|
></ha-logbook>
|
||||||
<div class="title">
|
|
||||||
${this.hass.localize("ui.dialogs.more_info_control.logbook")}
|
|
||||||
</div>
|
|
||||||
<a href=${this._showMoreHref} @click=${this._close}
|
|
||||||
>${this.hass.localize(
|
|
||||||
"ui.dialogs.more_info_control.show_more"
|
|
||||||
)}</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<ha-logbook
|
|
||||||
narrow
|
|
||||||
no-icon
|
|
||||||
no-name
|
|
||||||
relative-time
|
|
||||||
.hass=${this.hass}
|
|
||||||
.entries=${this._logbookEntries}
|
|
||||||
.traceContexts=${this._traceContexts}
|
|
||||||
.userIdToName=${this._userIdToName}
|
|
||||||
></ha-logbook>
|
|
||||||
`
|
|
||||||
: html`<div class="no-entries">
|
|
||||||
${this.hass.localize("ui.components.logbook.entries_not_found")}
|
|
||||||
</div>`
|
|
||||||
: ""}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated(): void {
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
this._fetchUserPromise = this._fetchUserNames();
|
super.willUpdate(changedProps);
|
||||||
}
|
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues): void {
|
|
||||||
super.updated(changedProps);
|
|
||||||
|
|
||||||
if (changedProps.has("entityId")) {
|
|
||||||
this._lastLogbookDate = undefined;
|
|
||||||
this._logbookEntries = undefined;
|
|
||||||
|
|
||||||
if (!this.entityId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (changedProps.has("entityId") && this.entityId) {
|
||||||
this._showMoreHref = `/logbook?entity_id=${
|
this._showMoreHref = `/logbook?entity_id=${
|
||||||
this.entityId
|
this.entityId
|
||||||
}&start_date=${startOfYesterday().toISOString()}`;
|
}&start_date=${startOfYesterday().toISOString()}`;
|
||||||
|
|
||||||
this._throttleGetLogbookEntries();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.entityId || !changedProps.has("hass")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
|
||||||
|
|
||||||
if (
|
|
||||||
oldHass &&
|
|
||||||
this.hass.states[this.entityId] !== oldHass?.states[this.entityId]
|
|
||||||
) {
|
|
||||||
// wait for commit of data (we only account for the default setting of 1 sec)
|
|
||||||
setTimeout(this._throttleGetLogbookEntries, 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _getLogBookData() {
|
|
||||||
if (!isComponentLoaded(this.hass, "logbook")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const lastDate =
|
|
||||||
this._lastLogbookDate ||
|
|
||||||
new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
|
|
||||||
const now = new Date();
|
|
||||||
let newEntries;
|
|
||||||
let traceContexts;
|
|
||||||
|
|
||||||
try {
|
|
||||||
[newEntries, traceContexts] = await Promise.all([
|
|
||||||
getLogbookData(
|
|
||||||
this.hass,
|
|
||||||
lastDate.toISOString(),
|
|
||||||
now.toISOString(),
|
|
||||||
this.entityId
|
|
||||||
),
|
|
||||||
this.hass.user?.is_admin ? loadTraceContexts(this.hass) : {},
|
|
||||||
this._fetchUserPromise,
|
|
||||||
]);
|
|
||||||
} catch (err: any) {
|
|
||||||
this._error = err.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._logbookEntries = this._logbookEntries
|
|
||||||
? [...newEntries, ...this._logbookEntries]
|
|
||||||
: newEntries;
|
|
||||||
this._lastLogbookDate = now;
|
|
||||||
this._traceContexts = traceContexts;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _fetchUserNames() {
|
|
||||||
const userIdToName = {};
|
|
||||||
|
|
||||||
// Start loading users
|
|
||||||
const userProm = this.hass.user?.is_admin && fetchUsers(this.hass);
|
|
||||||
|
|
||||||
// Process persons
|
|
||||||
Object.values(this.hass.states).forEach((entity) => {
|
|
||||||
if (
|
|
||||||
entity.attributes.user_id &&
|
|
||||||
computeStateDomain(entity) === "person"
|
|
||||||
) {
|
|
||||||
this._userIdToName[entity.attributes.user_id] =
|
|
||||||
entity.attributes.friendly_name;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process users
|
|
||||||
if (userProm) {
|
|
||||||
const users = await userProm;
|
|
||||||
for (const user of users) {
|
|
||||||
if (!(user.id in userIdToName)) {
|
|
||||||
userIdToName[user.id] = user.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._userIdToName = userIdToName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _close(): void {
|
private _close(): void {
|
||||||
@@ -199,13 +66,7 @@ export class MoreInfoLogbook extends LitElement {
|
|||||||
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
|
||||||
css`
|
css`
|
||||||
.no-entries {
|
|
||||||
text-align: center;
|
|
||||||
padding: 16px;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
ha-logbook {
|
ha-logbook {
|
||||||
--logbook-max-height: 250px;
|
--logbook-max-height: 250px;
|
||||||
}
|
}
|
||||||
@@ -214,10 +75,6 @@ export class MoreInfoLogbook extends LitElement {
|
|||||||
--logbook-max-height: unset;
|
--logbook-max-height: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ha-circular-progress {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@@ -49,12 +49,14 @@ class OnboardingIntegrations extends LitElement {
|
|||||||
this.hass.loadBackendTranslation("title", undefined, true);
|
this.hass.loadBackendTranslation("title", undefined, true);
|
||||||
this._unsubEvents = subscribeConfigFlowInProgress(this.hass, (flows) => {
|
this._unsubEvents = subscribeConfigFlowInProgress(this.hass, (flows) => {
|
||||||
this._discovered = flows;
|
this._discovered = flows;
|
||||||
|
const integrations: Set<string> = new Set();
|
||||||
for (const flow of flows) {
|
for (const flow of flows) {
|
||||||
// To render title placeholders
|
// To render title placeholders
|
||||||
if (flow.context.title_placeholders) {
|
if (flow.context.title_placeholders) {
|
||||||
this.hass.loadBackendTranslation("config", flow.handler);
|
integrations.add(flow.handler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.hass.loadBackendTranslation("config", Array.from(integrations));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -336,6 +336,9 @@ export class HAFullCalendar extends LitElement {
|
|||||||
|
|
||||||
.today {
|
.today {
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
|
margin-inline-end: 20px;
|
||||||
|
margin-inline-start: initial;
|
||||||
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
.prev,
|
.prev,
|
||||||
|
@@ -194,10 +194,13 @@ class PanelCalendar extends LitElement {
|
|||||||
|
|
||||||
.calendar-list {
|
.calendar-list {
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
|
padding-inline-end: 16px;
|
||||||
|
padding-inline-start: initial;
|
||||||
min-width: 170px;
|
min-width: 170px;
|
||||||
flex: 0 0 15%;
|
flex: 0 0 15%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||||
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-list > div {
|
.calendar-list > div {
|
||||||
|
@@ -41,6 +41,8 @@ export class DialogAddApplicationCredential extends LitElement {
|
|||||||
|
|
||||||
@state() private _domain?: string;
|
@state() private _domain?: string;
|
||||||
|
|
||||||
|
@state() private _name?: string;
|
||||||
|
|
||||||
@state() private _clientId?: string;
|
@state() private _clientId?: string;
|
||||||
|
|
||||||
@state() private _clientSecret?: string;
|
@state() private _clientSecret?: string;
|
||||||
@@ -49,7 +51,9 @@ export class DialogAddApplicationCredential extends LitElement {
|
|||||||
|
|
||||||
public showDialog(params: AddApplicationCredentialDialogParams) {
|
public showDialog(params: AddApplicationCredentialDialogParams) {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
this._domain = "";
|
this._domain =
|
||||||
|
params.selectedDomain !== undefined ? params.selectedDomain : "";
|
||||||
|
this._name = "";
|
||||||
this._clientId = "";
|
this._clientId = "";
|
||||||
this._clientSecret = "";
|
this._clientSecret = "";
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
@@ -72,7 +76,7 @@ export class DialogAddApplicationCredential extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<ha-dialog
|
<ha-dialog
|
||||||
open
|
open
|
||||||
@closed=${this.closeDialog}
|
@closed=${this._abortDialog}
|
||||||
scrimClickAction
|
scrimClickAction
|
||||||
escapeKeyAction
|
escapeKeyAction
|
||||||
.heading=${createCloseHeading(
|
.heading=${createCloseHeading(
|
||||||
@@ -87,6 +91,7 @@ export class DialogAddApplicationCredential extends LitElement {
|
|||||||
<ha-combo-box
|
<ha-combo-box
|
||||||
name="domain"
|
name="domain"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
.disabled=${!!this._params.selectedDomain}
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.panel.config.application_credentials.editor.domain"
|
"ui.panel.config.application_credentials.editor.domain"
|
||||||
)}
|
)}
|
||||||
@@ -99,6 +104,18 @@ export class DialogAddApplicationCredential extends LitElement {
|
|||||||
required
|
required
|
||||||
@value-changed=${this._handleDomainPicked}
|
@value-changed=${this._handleDomainPicked}
|
||||||
></ha-combo-box>
|
></ha-combo-box>
|
||||||
|
<ha-textfield
|
||||||
|
class="name"
|
||||||
|
name="name"
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.application_credentials.editor.name"
|
||||||
|
)}
|
||||||
|
.value=${this._name}
|
||||||
|
required
|
||||||
|
@input=${this._handleValueChanged}
|
||||||
|
error-message=${this.hass.localize("ui.common.error_required")}
|
||||||
|
dialogInitialFocus
|
||||||
|
></ha-textfield>
|
||||||
<ha-textfield
|
<ha-textfield
|
||||||
class="clientId"
|
class="clientId"
|
||||||
name="clientId"
|
name="clientId"
|
||||||
@@ -166,6 +183,13 @@ export class DialogAddApplicationCredential extends LitElement {
|
|||||||
this[`_${name}`] = value;
|
this[`_${name}`] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _abortDialog() {
|
||||||
|
if (this._params && this._params.dialogAbortedCallback) {
|
||||||
|
this._params.dialogAbortedCallback();
|
||||||
|
}
|
||||||
|
this.closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
private async _createApplicationCredential(ev) {
|
private async _createApplicationCredential(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (!this._domain || !this._clientId || !this._clientSecret) {
|
if (!this._domain || !this._clientId || !this._clientSecret) {
|
||||||
@@ -181,7 +205,8 @@ export class DialogAddApplicationCredential extends LitElement {
|
|||||||
this.hass,
|
this.hass,
|
||||||
this._domain,
|
this._domain,
|
||||||
this._clientId,
|
this._clientId,
|
||||||
this._clientSecret
|
this._clientSecret,
|
||||||
|
this._name
|
||||||
);
|
);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this._loading = false;
|
this._loading = false;
|
||||||
|
@@ -19,7 +19,10 @@ import {
|
|||||||
fetchApplicationCredentials,
|
fetchApplicationCredentials,
|
||||||
} from "../../../data/application_credential";
|
} from "../../../data/application_credential";
|
||||||
import { domainToName } from "../../../data/integration";
|
import { domainToName } from "../../../data/integration";
|
||||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
import {
|
||||||
|
showAlertDialog,
|
||||||
|
showConfirmationDialog,
|
||||||
|
} from "../../../dialogs/generic/show-dialog-box";
|
||||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||||
import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table";
|
import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table";
|
||||||
import { HomeAssistant, Route } from "../../../types";
|
import { HomeAssistant, Route } from "../../../types";
|
||||||
@@ -46,13 +49,22 @@ export class HaConfigApplicationCredentials extends LitElement {
|
|||||||
private _columns = memoizeOne(
|
private _columns = memoizeOne(
|
||||||
(narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => {
|
(narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => {
|
||||||
const columns: DataTableColumnContainer<ApplicationCredential> = {
|
const columns: DataTableColumnContainer<ApplicationCredential> = {
|
||||||
|
name: {
|
||||||
|
title: localize(
|
||||||
|
"ui.panel.config.application_credentials.picker.headers.name"
|
||||||
|
),
|
||||||
|
width: "40%",
|
||||||
|
direction: "asc",
|
||||||
|
grows: true,
|
||||||
|
template: (_, entry: ApplicationCredential) => html`${entry.name}`,
|
||||||
|
},
|
||||||
clientId: {
|
clientId: {
|
||||||
title: localize(
|
title: localize(
|
||||||
"ui.panel.config.application_credentials.picker.headers.client_id"
|
"ui.panel.config.application_credentials.picker.headers.client_id"
|
||||||
),
|
),
|
||||||
width: "25%",
|
width: "30%",
|
||||||
direction: "asc",
|
direction: "asc",
|
||||||
grows: true,
|
hidden: narrow,
|
||||||
template: (_, entry: ApplicationCredential) =>
|
template: (_, entry: ApplicationCredential) =>
|
||||||
html`${entry.client_id}`,
|
html`${entry.client_id}`,
|
||||||
},
|
},
|
||||||
@@ -61,9 +73,8 @@ export class HaConfigApplicationCredentials extends LitElement {
|
|||||||
"ui.panel.config.application_credentials.picker.headers.application"
|
"ui.panel.config.application_credentials.picker.headers.application"
|
||||||
),
|
),
|
||||||
sortable: true,
|
sortable: true,
|
||||||
width: "20%",
|
width: "30%",
|
||||||
direction: "asc",
|
direction: "asc",
|
||||||
hidden: narrow,
|
|
||||||
template: (_, entry) => html`${domainToName(localize, entry.domain)}`,
|
template: (_, entry) => html`${domainToName(localize, entry.domain)}`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -171,11 +182,24 @@ export class HaConfigApplicationCredentials extends LitElement {
|
|||||||
confirmText: this.hass.localize("ui.common.remove"),
|
confirmText: this.hass.localize("ui.common.remove"),
|
||||||
dismissText: this.hass.localize("ui.common.cancel"),
|
dismissText: this.hass.localize("ui.common.cancel"),
|
||||||
confirm: async () => {
|
confirm: async () => {
|
||||||
await Promise.all(
|
try {
|
||||||
this._selected.map(async (applicationCredential) => {
|
await Promise.all(
|
||||||
await deleteApplicationCredential(this.hass, applicationCredential);
|
this._selected.map(async (applicationCredential) => {
|
||||||
})
|
await deleteApplicationCredential(
|
||||||
);
|
this.hass,
|
||||||
|
applicationCredential
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (err: any) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.application_credentials.picker.remove_selected.error_title"
|
||||||
|
),
|
||||||
|
text: err.message,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._dataTable.clearSelection();
|
this._dataTable.clearSelection();
|
||||||
this._fetchApplicationCredentials();
|
this._fetchApplicationCredentials();
|
||||||
},
|
},
|
||||||
@@ -228,6 +252,8 @@ export class HaConfigApplicationCredentials extends LitElement {
|
|||||||
.selected-txt {
|
.selected-txt {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
|
padding-inline-start: 16px;
|
||||||
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
.table-header .selected-txt {
|
.table-header .selected-txt {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
@@ -5,6 +5,8 @@ export interface AddApplicationCredentialDialogParams {
|
|||||||
applicationCredentialAddedCallback: (
|
applicationCredentialAddedCallback: (
|
||||||
applicationCredential: ApplicationCredential
|
applicationCredential: ApplicationCredential
|
||||||
) => void;
|
) => void;
|
||||||
|
dialogAbortedCallback?: () => void;
|
||||||
|
selectedDomain?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadAddApplicationCredentialDialog = () =>
|
export const loadAddApplicationCredentialDialog = () =>
|
||||||
|
@@ -2,7 +2,10 @@ import "@material/mwc-button";
|
|||||||
import { mdiImagePlus, mdiPencil } from "@mdi/js";
|
import { mdiImagePlus, mdiPencil } from "@mdi/js";
|
||||||
import "@polymer/paper-item/paper-item";
|
import "@polymer/paper-item/paper-item";
|
||||||
import "@polymer/paper-item/paper-item-body";
|
import "@polymer/paper-item/paper-item-body";
|
||||||
import { HassEntity } from "home-assistant-js-websocket/dist/types";
|
import {
|
||||||
|
HassEntity,
|
||||||
|
UnsubscribeFunc,
|
||||||
|
} from "home-assistant-js-websocket/dist/types";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
@@ -19,6 +22,7 @@ import "../../../components/ha-icon-next";
|
|||||||
import {
|
import {
|
||||||
AreaRegistryEntry,
|
AreaRegistryEntry,
|
||||||
deleteAreaRegistryEntry,
|
deleteAreaRegistryEntry,
|
||||||
|
subscribeAreaRegistry,
|
||||||
updateAreaRegistryEntry,
|
updateAreaRegistryEntry,
|
||||||
} from "../../../data/area_registry";
|
} from "../../../data/area_registry";
|
||||||
import { AutomationEntity } from "../../../data/automation";
|
import { AutomationEntity } from "../../../data/automation";
|
||||||
@@ -26,18 +30,22 @@ import {
|
|||||||
computeDeviceName,
|
computeDeviceName,
|
||||||
DeviceRegistryEntry,
|
DeviceRegistryEntry,
|
||||||
sortDeviceRegistryByName,
|
sortDeviceRegistryByName,
|
||||||
|
subscribeDeviceRegistry,
|
||||||
} from "../../../data/device_registry";
|
} from "../../../data/device_registry";
|
||||||
import {
|
import {
|
||||||
computeEntityRegistryName,
|
computeEntityRegistryName,
|
||||||
EntityRegistryEntry,
|
EntityRegistryEntry,
|
||||||
sortEntityRegistryByName,
|
sortEntityRegistryByName,
|
||||||
|
subscribeEntityRegistry,
|
||||||
} from "../../../data/entity_registry";
|
} from "../../../data/entity_registry";
|
||||||
import { SceneEntity } from "../../../data/scene";
|
import { SceneEntity } from "../../../data/scene";
|
||||||
import { ScriptEntity } from "../../../data/script";
|
import { ScriptEntity } from "../../../data/script";
|
||||||
import { findRelated, RelatedResult } from "../../../data/search";
|
import { findRelated, RelatedResult } from "../../../data/search";
|
||||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||||
|
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||||
import { haStyle } from "../../../resources/styles";
|
import { haStyle } from "../../../resources/styles";
|
||||||
import { HomeAssistant, Route } from "../../../types";
|
import { HomeAssistant, Route } from "../../../types";
|
||||||
|
import "../../logbook/ha-logbook";
|
||||||
import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor";
|
import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor";
|
||||||
import { configSections } from "../ha-panel-config";
|
import { configSections } from "../ha-panel-config";
|
||||||
import {
|
import {
|
||||||
@@ -51,17 +59,11 @@ declare type NameAndEntity<EntityType extends HassEntity> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@customElement("ha-config-area-page")
|
@customElement("ha-config-area-page")
|
||||||
class HaConfigAreaPage extends LitElement {
|
class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() public areaId!: string;
|
@property() public areaId!: string;
|
||||||
|
|
||||||
@property() public areas!: AreaRegistryEntry[];
|
|
||||||
|
|
||||||
@property() public devices!: DeviceRegistryEntry[];
|
|
||||||
|
|
||||||
@property() public entities!: EntityRegistryEntry[];
|
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||||
|
|
||||||
@property() public isWide!: boolean;
|
@property() public isWide!: boolean;
|
||||||
@@ -70,8 +72,16 @@ class HaConfigAreaPage extends LitElement {
|
|||||||
|
|
||||||
@property() public route!: Route;
|
@property() public route!: Route;
|
||||||
|
|
||||||
|
@state() public _areas!: AreaRegistryEntry[];
|
||||||
|
|
||||||
|
@state() public _devices!: DeviceRegistryEntry[];
|
||||||
|
|
||||||
|
@state() public _entities!: EntityRegistryEntry[];
|
||||||
|
|
||||||
@state() private _related?: RelatedResult;
|
@state() private _related?: RelatedResult;
|
||||||
|
|
||||||
|
private _logbookTime = { recent: 86400 };
|
||||||
|
|
||||||
private _area = memoizeOne(
|
private _area = memoizeOne(
|
||||||
(
|
(
|
||||||
areaId: string,
|
areaId: string,
|
||||||
@@ -86,7 +96,7 @@ class HaConfigAreaPage extends LitElement {
|
|||||||
registryDevices: DeviceRegistryEntry[],
|
registryDevices: DeviceRegistryEntry[],
|
||||||
registryEntities: EntityRegistryEntry[]
|
registryEntities: EntityRegistryEntry[]
|
||||||
) => {
|
) => {
|
||||||
const devices = new Map();
|
const devices = new Map<string, DeviceRegistryEntry>();
|
||||||
|
|
||||||
for (const device of registryDevices) {
|
for (const device of registryDevices) {
|
||||||
if (device.area_id === areaId) {
|
if (device.area_id === areaId) {
|
||||||
@@ -102,7 +112,7 @@ class HaConfigAreaPage extends LitElement {
|
|||||||
if (entity.area_id === areaId) {
|
if (entity.area_id === areaId) {
|
||||||
entities.push(entity);
|
entities.push(entity);
|
||||||
}
|
}
|
||||||
} else if (devices.has(entity.device_id)) {
|
} else if (entity.device_id && devices.has(entity.device_id)) {
|
||||||
indirectEntities.push(entity);
|
indirectEntities.push(entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,6 +125,20 @@ class HaConfigAreaPage extends LitElement {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private _allDeviceIds = memoizeOne((devices: DeviceRegistryEntry[]) =>
|
||||||
|
devices.map((device) => device.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
private _allEntities = memoizeOne(
|
||||||
|
(memberships: {
|
||||||
|
entities: EntityRegistryEntry[];
|
||||||
|
indirectEntities: EntityRegistryEntry[];
|
||||||
|
}) =>
|
||||||
|
memberships.entities
|
||||||
|
.map((entry) => entry.entity_id)
|
||||||
|
.concat(memberships.indirectEntities.map((entry) => entry.entity_id))
|
||||||
|
);
|
||||||
|
|
||||||
protected firstUpdated(changedProps) {
|
protected firstUpdated(changedProps) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
loadAreaRegistryDetailDialog();
|
loadAreaRegistryDetailDialog();
|
||||||
@@ -127,8 +151,26 @@ class HaConfigAreaPage extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||||
|
return [
|
||||||
|
subscribeAreaRegistry(this.hass.connection, (areas) => {
|
||||||
|
this._areas = areas;
|
||||||
|
}),
|
||||||
|
subscribeDeviceRegistry(this.hass.connection, (entries) => {
|
||||||
|
this._devices = entries;
|
||||||
|
}),
|
||||||
|
subscribeEntityRegistry(this.hass.connection, (entries) => {
|
||||||
|
this._entities = entries;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const area = this._area(this.areaId, this.areas);
|
if (!this._areas || !this._devices || !this._entities) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
const area = this._area(this.areaId, this._areas);
|
||||||
|
|
||||||
if (!area) {
|
if (!area) {
|
||||||
return html`
|
return html`
|
||||||
@@ -139,11 +181,12 @@ class HaConfigAreaPage extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { devices, entities } = this._memberships(
|
const memberships = this._memberships(
|
||||||
this.areaId,
|
this.areaId,
|
||||||
this.devices,
|
this._devices,
|
||||||
this.entities
|
this._entities
|
||||||
);
|
);
|
||||||
|
const { devices, entities } = memberships;
|
||||||
|
|
||||||
// Pre-compute the entity and device names, so we can sort by them
|
// Pre-compute the entity and device names, so we can sort by them
|
||||||
if (devices) {
|
if (devices) {
|
||||||
@@ -359,8 +402,6 @@ class HaConfigAreaPage extends LitElement {
|
|||||||
</ha-card>
|
</ha-card>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
${isComponentLoaded(this.hass, "scene")
|
${isComponentLoaded(this.hass, "scene")
|
||||||
? html`
|
? html`
|
||||||
<ha-card
|
<ha-card
|
||||||
@@ -442,6 +483,26 @@ class HaConfigAreaPage extends LitElement {
|
|||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
${isComponentLoaded(this.hass, "logbook")
|
||||||
|
? html`
|
||||||
|
<ha-card
|
||||||
|
outlined
|
||||||
|
.header=${this.hass.localize("panel.logbook")}
|
||||||
|
>
|
||||||
|
<ha-logbook
|
||||||
|
.hass=${this.hass}
|
||||||
|
.time=${this._logbookTime}
|
||||||
|
.entityIds=${this._allEntities(memberships)}
|
||||||
|
.deviceIds=${this._allDeviceIds(memberships.devices)}
|
||||||
|
virtualize
|
||||||
|
narrow
|
||||||
|
no-icon
|
||||||
|
></ha-logbook>
|
||||||
|
</ha-card>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</hass-tabs-subpage>
|
</hass-tabs-subpage>
|
||||||
`;
|
`;
|
||||||
@@ -699,6 +760,13 @@ class HaConfigAreaPage extends LitElement {
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
ha-logbook {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
:host([narrow]) ha-logbook {
|
||||||
|
height: 235px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { mdiHelpCircle, mdiPlus } from "@mdi/js";
|
import { mdiHelpCircle, mdiPlus } from "@mdi/js";
|
||||||
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import "../../../components/ha-fab";
|
import "../../../components/ha-fab";
|
||||||
@@ -9,12 +10,20 @@ import "../../../components/ha-svg-icon";
|
|||||||
import {
|
import {
|
||||||
AreaRegistryEntry,
|
AreaRegistryEntry,
|
||||||
createAreaRegistryEntry,
|
createAreaRegistryEntry,
|
||||||
|
subscribeAreaRegistry,
|
||||||
} from "../../../data/area_registry";
|
} from "../../../data/area_registry";
|
||||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
import {
|
||||||
import type { EntityRegistryEntry } from "../../../data/entity_registry";
|
DeviceRegistryEntry,
|
||||||
|
subscribeDeviceRegistry,
|
||||||
|
} from "../../../data/device_registry";
|
||||||
|
import {
|
||||||
|
EntityRegistryEntry,
|
||||||
|
subscribeEntityRegistry,
|
||||||
|
} from "../../../data/entity_registry";
|
||||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||||
import "../../../layouts/hass-loading-screen";
|
import "../../../layouts/hass-loading-screen";
|
||||||
import "../../../layouts/hass-tabs-subpage";
|
import "../../../layouts/hass-tabs-subpage";
|
||||||
|
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||||
import { HomeAssistant, Route } from "../../../types";
|
import { HomeAssistant, Route } from "../../../types";
|
||||||
import "../ha-config-section";
|
import "../ha-config-section";
|
||||||
import { configSections } from "../ha-panel-config";
|
import { configSections } from "../ha-panel-config";
|
||||||
@@ -24,7 +33,7 @@ import {
|
|||||||
} from "./show-dialog-area-registry-detail";
|
} from "./show-dialog-area-registry-detail";
|
||||||
|
|
||||||
@customElement("ha-config-areas-dashboard")
|
@customElement("ha-config-areas-dashboard")
|
||||||
export class HaConfigAreasDashboard extends LitElement {
|
export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() public isWide?: boolean;
|
@property() public isWide?: boolean;
|
||||||
@@ -33,13 +42,13 @@ export class HaConfigAreasDashboard extends LitElement {
|
|||||||
|
|
||||||
@property() public route!: Route;
|
@property() public route!: Route;
|
||||||
|
|
||||||
@property() public areas!: AreaRegistryEntry[];
|
@state() private _areas!: AreaRegistryEntry[];
|
||||||
|
|
||||||
@property() public devices!: DeviceRegistryEntry[];
|
@state() private _devices!: DeviceRegistryEntry[];
|
||||||
|
|
||||||
@property() public entities!: EntityRegistryEntry[];
|
@state() private _entities!: EntityRegistryEntry[];
|
||||||
|
|
||||||
private _areas = memoizeOne(
|
private _processAreas = memoizeOne(
|
||||||
(
|
(
|
||||||
areas: AreaRegistryEntry[],
|
areas: AreaRegistryEntry[],
|
||||||
devices: DeviceRegistryEntry[],
|
devices: DeviceRegistryEntry[],
|
||||||
@@ -75,6 +84,20 @@ export class HaConfigAreasDashboard extends LitElement {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||||
|
return [
|
||||||
|
subscribeAreaRegistry(this.hass.connection, (areas) => {
|
||||||
|
this._areas = areas;
|
||||||
|
}),
|
||||||
|
subscribeDeviceRegistry(this.hass.connection, (entries) => {
|
||||||
|
this._devices = entries;
|
||||||
|
}),
|
||||||
|
subscribeEntityRegistry(this.hass.connection, (entries) => {
|
||||||
|
this._entities = entries;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<hass-tabs-subpage
|
<hass-tabs-subpage
|
||||||
@@ -92,56 +115,62 @@ export class HaConfigAreasDashboard extends LitElement {
|
|||||||
@click=${this._showHelp}
|
@click=${this._showHelp}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
${this._areas(this.areas, this.devices, this.entities).map(
|
${!this._areas || !this._devices || !this._entities
|
||||||
(area) =>
|
? ""
|
||||||
html`<a href=${`/config/areas/area/${area.area_id}`}
|
: this._processAreas(
|
||||||
><ha-card outlined>
|
this._areas,
|
||||||
<div
|
this._devices,
|
||||||
style=${styleMap({
|
this._entities
|
||||||
backgroundImage: area.picture
|
).map(
|
||||||
? `url(${area.picture})`
|
(area) =>
|
||||||
: undefined,
|
html`<a href=${`/config/areas/area/${area.area_id}`}
|
||||||
})}
|
><ha-card outlined>
|
||||||
class="picture ${!area.picture ? "placeholder" : ""}"
|
<div
|
||||||
></div>
|
style=${styleMap({
|
||||||
<h1 class="card-header">${area.name}</h1>
|
backgroundImage: area.picture
|
||||||
<div class="card-content">
|
? `url(${area.picture})`
|
||||||
<div>
|
: undefined,
|
||||||
${area.devices
|
})}
|
||||||
? html`
|
class="picture ${!area.picture ? "placeholder" : ""}"
|
||||||
${this.hass.localize(
|
></div>
|
||||||
"ui.panel.config.integrations.config_entry.devices",
|
<h1 class="card-header">${area.name}</h1>
|
||||||
"count",
|
<div class="card-content">
|
||||||
area.devices
|
<div>
|
||||||
)}${area.services ? "," : ""}
|
${area.devices
|
||||||
`
|
? html`
|
||||||
: ""}
|
${this.hass.localize(
|
||||||
${area.services
|
"ui.panel.config.integrations.config_entry.devices",
|
||||||
? html`
|
"count",
|
||||||
${this.hass.localize(
|
area.devices
|
||||||
"ui.panel.config.integrations.config_entry.services",
|
)}${area.services ? "," : ""}
|
||||||
"count",
|
`
|
||||||
area.services
|
: ""}
|
||||||
)}
|
${area.services
|
||||||
`
|
? html`
|
||||||
: ""}
|
${this.hass.localize(
|
||||||
${(area.devices || area.services) && area.entities
|
"ui.panel.config.integrations.config_entry.services",
|
||||||
? this.hass.localize("ui.common.and")
|
"count",
|
||||||
: ""}
|
area.services
|
||||||
${area.entities
|
)}
|
||||||
? html`
|
`
|
||||||
${this.hass.localize(
|
: ""}
|
||||||
"ui.panel.config.integrations.config_entry.entities",
|
${(area.devices || area.services) && area.entities
|
||||||
"count",
|
? this.hass.localize("ui.common.and")
|
||||||
area.entities
|
: ""}
|
||||||
)}
|
${area.entities
|
||||||
`
|
? html`
|
||||||
: ""}
|
${this.hass.localize(
|
||||||
</div>
|
"ui.panel.config.integrations.config_entry.entities",
|
||||||
</div>
|
"count",
|
||||||
</ha-card></a
|
area.entities
|
||||||
>`
|
)}
|
||||||
)}
|
`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ha-card></a
|
||||||
|
>`
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ha-fab
|
<ha-fab
|
||||||
slot="fab"
|
slot="fab"
|
||||||
|
@@ -1,20 +1,4 @@
|
|||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { PropertyValues } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import { stringCompare } from "../../../common/string/compare";
|
|
||||||
import {
|
|
||||||
AreaRegistryEntry,
|
|
||||||
subscribeAreaRegistry,
|
|
||||||
} from "../../../data/area_registry";
|
|
||||||
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
|
|
||||||
import {
|
|
||||||
DeviceRegistryEntry,
|
|
||||||
subscribeDeviceRegistry,
|
|
||||||
} from "../../../data/device_registry";
|
|
||||||
import {
|
|
||||||
EntityRegistryEntry,
|
|
||||||
subscribeEntityRegistry,
|
|
||||||
} from "../../../data/entity_registry";
|
|
||||||
import {
|
import {
|
||||||
HassRouterPage,
|
HassRouterPage,
|
||||||
RouterOptions,
|
RouterOptions,
|
||||||
@@ -46,44 +30,6 @@ class HaConfigAreas extends HassRouterPage {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@state() private _configEntries: ConfigEntry[] = [];
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private _deviceRegistryEntries: DeviceRegistryEntry[] = [];
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private _entityRegistryEntries: EntityRegistryEntry[] = [];
|
|
||||||
|
|
||||||
@state() private _areas: AreaRegistryEntry[] = [];
|
|
||||||
|
|
||||||
private _unsubs?: UnsubscribeFunc[];
|
|
||||||
|
|
||||||
public connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
|
|
||||||
if (!this.hass) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
public disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
if (this._unsubs) {
|
|
||||||
while (this._unsubs.length) {
|
|
||||||
this._unsubs.pop()!();
|
|
||||||
}
|
|
||||||
this._unsubs = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues) {
|
|
||||||
super.updated(changedProps);
|
|
||||||
if (!this._unsubs && changedProps.has("hass")) {
|
|
||||||
this._loadData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected updatePageEl(pageEl) {
|
protected updatePageEl(pageEl) {
|
||||||
pageEl.hass = this.hass;
|
pageEl.hass = this.hass;
|
||||||
|
|
||||||
@@ -91,37 +37,11 @@ class HaConfigAreas extends HassRouterPage {
|
|||||||
pageEl.areaId = this.routeTail.path.substr(1);
|
pageEl.areaId = this.routeTail.path.substr(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
pageEl.entries = this._configEntries;
|
|
||||||
pageEl.devices = this._deviceRegistryEntries;
|
|
||||||
pageEl.entities = this._entityRegistryEntries;
|
|
||||||
pageEl.areas = this._areas;
|
|
||||||
pageEl.narrow = this.narrow;
|
pageEl.narrow = this.narrow;
|
||||||
pageEl.isWide = this.isWide;
|
pageEl.isWide = this.isWide;
|
||||||
pageEl.showAdvanced = this.showAdvanced;
|
pageEl.showAdvanced = this.showAdvanced;
|
||||||
pageEl.route = this.routeTail;
|
pageEl.route = this.routeTail;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _loadData() {
|
|
||||||
getConfigEntries(this.hass).then((configEntries) => {
|
|
||||||
this._configEntries = configEntries.sort((conf1, conf2) =>
|
|
||||||
stringCompare(conf1.title, conf2.title)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if (this._unsubs) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._unsubs = [
|
|
||||||
subscribeAreaRegistry(this.hass.connection, (areas) => {
|
|
||||||
this._areas = areas;
|
|
||||||
}),
|
|
||||||
subscribeDeviceRegistry(this.hass.connection, (entries) => {
|
|
||||||
this._deviceRegistryEntries = entries;
|
|
||||||
}),
|
|
||||||
subscribeEntityRegistry(this.hass.connection, (entries) => {
|
|
||||||
this._entityRegistryEntries = entries;
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@@ -0,0 +1,13 @@
|
|||||||
|
import type { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
||||||
|
import type { DeviceAction } from "../../../ha-config-device-page";
|
||||||
|
import { showMQTTDeviceDebugInfoDialog } from "./show-dialog-mqtt-device-debug-info";
|
||||||
|
|
||||||
|
export const getMQTTDeviceActions = (
|
||||||
|
el: HTMLElement,
|
||||||
|
device: DeviceRegistryEntry
|
||||||
|
): DeviceAction[] => [
|
||||||
|
{
|
||||||
|
label: "MQTT Info",
|
||||||
|
action: async () => showMQTTDeviceDebugInfoDialog(el, { device }),
|
||||||
|
},
|
||||||
|
];
|
@@ -1,36 +0,0 @@
|
|||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
|
||||||
import { customElement, property } from "lit/decorators";
|
|
||||||
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
|
||||||
import { haStyle } from "../../../../../../resources/styles";
|
|
||||||
import { HomeAssistant } from "../../../../../../types";
|
|
||||||
import { showMQTTDeviceDebugInfoDialog } from "./show-dialog-mqtt-device-debug-info";
|
|
||||||
|
|
||||||
@customElement("ha-device-actions-mqtt")
|
|
||||||
export class HaDeviceActionsMqtt extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property() public device!: DeviceRegistryEntry;
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
return html`
|
|
||||||
<mwc-button @click=${this._showDebugInfo}> MQTT Info </mwc-button>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _showDebugInfo(): Promise<void> {
|
|
||||||
const device = this.device;
|
|
||||||
await showMQTTDeviceDebugInfoDialog(this, { device });
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return [
|
|
||||||
haStyle,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,108 @@
|
|||||||
|
import { navigate } from "../../../../../../common/navigate";
|
||||||
|
import type { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
||||||
|
import { fetchZHADevice } from "../../../../../../data/zha";
|
||||||
|
import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box";
|
||||||
|
import type { HomeAssistant } from "../../../../../../types";
|
||||||
|
import { showZHAClusterDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-cluster";
|
||||||
|
import { showZHADeviceChildrenDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-children";
|
||||||
|
import { showZHADeviceZigbeeInfoDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info";
|
||||||
|
import { showZHAReconfigureDeviceDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-reconfigure-device";
|
||||||
|
import type { DeviceAction } from "../../../ha-config-device-page";
|
||||||
|
|
||||||
|
export const getZHADeviceActions = async (
|
||||||
|
el: HTMLElement,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
device: DeviceRegistryEntry
|
||||||
|
): Promise<DeviceAction[]> => {
|
||||||
|
const zigbeeConnection = device.connections.find(
|
||||||
|
(conn) => conn[0] === "zigbee"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!zigbeeConnection) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const zhaDevice = await fetchZHADevice(hass, zigbeeConnection[1]);
|
||||||
|
|
||||||
|
if (!zhaDevice) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions: DeviceAction[] = [];
|
||||||
|
|
||||||
|
if (zhaDevice.device_type !== "Coordinator") {
|
||||||
|
actions.push({
|
||||||
|
label: hass.localize("ui.dialogs.zha_device_info.buttons.reconfigure"),
|
||||||
|
action: () => showZHAReconfigureDeviceDialog(el, { device: zhaDevice }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
zhaDevice.power_source === "Mains" &&
|
||||||
|
(zhaDevice.device_type === "Router" ||
|
||||||
|
zhaDevice.device_type === "Coordinator")
|
||||||
|
) {
|
||||||
|
actions.push(
|
||||||
|
...[
|
||||||
|
{
|
||||||
|
label: hass.localize("ui.dialogs.zha_device_info.buttons.add"),
|
||||||
|
action: () => navigate(`/config/zha/add/${zhaDevice!.ieee}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: hass.localize(
|
||||||
|
"ui.dialogs.zha_device_info.buttons.device_children"
|
||||||
|
),
|
||||||
|
action: () => showZHADeviceChildrenDialog(el, { device: zhaDevice! }),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zhaDevice.device_type !== "Coordinator") {
|
||||||
|
actions.push(
|
||||||
|
...[
|
||||||
|
{
|
||||||
|
label: hass.localize(
|
||||||
|
"ui.dialogs.zha_device_info.buttons.zigbee_information"
|
||||||
|
),
|
||||||
|
action: () =>
|
||||||
|
showZHADeviceZigbeeInfoDialog(el, { device: zhaDevice }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: hass.localize("ui.dialogs.zha_device_info.buttons.clusters"),
|
||||||
|
action: () => showZHAClusterDialog(el, { device: zhaDevice }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: hass.localize(
|
||||||
|
"ui.dialogs.zha_device_info.buttons.view_in_visualization"
|
||||||
|
),
|
||||||
|
action: () =>
|
||||||
|
navigate(`/config/zha/visualization/${zhaDevice!.device_reg_id}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: hass.localize("ui.dialogs.zha_device_info.buttons.remove"),
|
||||||
|
classes: "warning",
|
||||||
|
action: async () => {
|
||||||
|
const confirmed = await showConfirmationDialog(el, {
|
||||||
|
text: hass.localize(
|
||||||
|
"ui.dialogs.zha_device_info.confirmations.remove"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.callService("zha", "remove", {
|
||||||
|
ieee: zhaDevice.ieee,
|
||||||
|
});
|
||||||
|
|
||||||
|
history.back();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
};
|
@@ -1,155 +0,0 @@
|
|||||||
import {
|
|
||||||
css,
|
|
||||||
CSSResultGroup,
|
|
||||||
html,
|
|
||||||
LitElement,
|
|
||||||
PropertyValues,
|
|
||||||
TemplateResult,
|
|
||||||
} from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import { navigate } from "../../../../../../common/navigate";
|
|
||||||
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
|
||||||
import { fetchZHADevice, ZHADevice } from "../../../../../../data/zha";
|
|
||||||
import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box";
|
|
||||||
import { haStyle } from "../../../../../../resources/styles";
|
|
||||||
import { HomeAssistant } from "../../../../../../types";
|
|
||||||
import { showZHAClusterDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-cluster";
|
|
||||||
import { showZHADeviceChildrenDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-children";
|
|
||||||
import { showZHADeviceZigbeeInfoDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info";
|
|
||||||
import { showZHAReconfigureDeviceDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-reconfigure-device";
|
|
||||||
|
|
||||||
@customElement("ha-device-actions-zha")
|
|
||||||
export class HaDeviceActionsZha extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property() public device!: DeviceRegistryEntry;
|
|
||||||
|
|
||||||
@state() private _zhaDevice?: ZHADevice;
|
|
||||||
|
|
||||||
protected updated(changedProperties: PropertyValues) {
|
|
||||||
if (changedProperties.has("device")) {
|
|
||||||
const zigbeeConnection = this.device.connections.find(
|
|
||||||
(conn) => conn[0] === "zigbee"
|
|
||||||
);
|
|
||||||
if (!zigbeeConnection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fetchZHADevice(this.hass, zigbeeConnection[1]).then((device) => {
|
|
||||||
this._zhaDevice = device;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
if (!this._zhaDevice) {
|
|
||||||
return html``;
|
|
||||||
}
|
|
||||||
return html`
|
|
||||||
${this._zhaDevice.device_type !== "Coordinator"
|
|
||||||
? html`
|
|
||||||
<mwc-button @click=${this._onReconfigureNodeClick}>
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.dialogs.zha_device_info.buttons.reconfigure"
|
|
||||||
)}
|
|
||||||
</mwc-button>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
${this._zhaDevice.power_source === "Mains" &&
|
|
||||||
(this._zhaDevice.device_type === "Router" ||
|
|
||||||
this._zhaDevice.device_type === "Coordinator")
|
|
||||||
? html`
|
|
||||||
<mwc-button @click=${this._onAddDevicesClick}>
|
|
||||||
${this.hass!.localize("ui.dialogs.zha_device_info.buttons.add")}
|
|
||||||
</mwc-button>
|
|
||||||
<mwc-button @click=${this._handleDeviceChildrenClicked}>
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.dialogs.zha_device_info.buttons.device_children"
|
|
||||||
)}
|
|
||||||
</mwc-button>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
${this._zhaDevice.device_type !== "Coordinator"
|
|
||||||
? html`
|
|
||||||
<mwc-button @click=${this._handleZigbeeInfoClicked}>
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.dialogs.zha_device_info.buttons.zigbee_information"
|
|
||||||
)}
|
|
||||||
</mwc-button>
|
|
||||||
<mwc-button @click=${this._showClustersDialog}>
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.dialogs.zha_device_info.buttons.clusters"
|
|
||||||
)}
|
|
||||||
</mwc-button>
|
|
||||||
<mwc-button @click=${this._onViewInVisualizationClick}>
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.dialogs.zha_device_info.buttons.view_in_visualization"
|
|
||||||
)}
|
|
||||||
</mwc-button>
|
|
||||||
<mwc-button class="warning" @click=${this._removeDevice}>
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.dialogs.zha_device_info.buttons.remove"
|
|
||||||
)}
|
|
||||||
</mwc-button>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _showClustersDialog(): Promise<void> {
|
|
||||||
await showZHAClusterDialog(this, { device: this._zhaDevice! });
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _onReconfigureNodeClick(): Promise<void> {
|
|
||||||
if (!this.hass) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showZHAReconfigureDeviceDialog(this, { device: this._zhaDevice! });
|
|
||||||
}
|
|
||||||
|
|
||||||
private _onAddDevicesClick() {
|
|
||||||
navigate(`/config/zha/add/${this._zhaDevice!.ieee}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _onViewInVisualizationClick() {
|
|
||||||
navigate(`/config/zha/visualization/${this._zhaDevice!.device_reg_id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _handleZigbeeInfoClicked() {
|
|
||||||
showZHADeviceZigbeeInfoDialog(this, { device: this._zhaDevice! });
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _handleDeviceChildrenClicked() {
|
|
||||||
showZHADeviceChildrenDialog(this, { device: this._zhaDevice! });
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _removeDevice() {
|
|
||||||
const confirmed = await showConfirmationDialog(this, {
|
|
||||||
text: this.hass.localize(
|
|
||||||
"ui.dialogs.zha_device_info.confirmations.remove"
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.hass.callService("zha", "remove", {
|
|
||||||
ieee: this._zhaDevice!.ieee,
|
|
||||||
});
|
|
||||||
|
|
||||||
history.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return [
|
|
||||||
haStyle,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
@@ -7,6 +7,7 @@ import {
|
|||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import "../../../../../../components/ha-expansion-panel";
|
||||||
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
||||||
import { fetchZHADevice, ZHADevice } from "../../../../../../data/zha";
|
import { fetchZHADevice, ZHADevice } from "../../../../../../data/zha";
|
||||||
import { haStyle } from "../../../../../../resources/styles";
|
import { haStyle } from "../../../../../../resources/styles";
|
||||||
@@ -40,38 +41,39 @@ export class HaDeviceActionsZha extends LitElement {
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<h4>Zigbee info</h4>
|
<ha-expansion-panel header="Zigbee info">
|
||||||
<div>IEEE: ${this._zhaDevice.ieee}</div>
|
<div>IEEE: ${this._zhaDevice.ieee}</div>
|
||||||
<div>Nwk: ${formatAsPaddedHex(this._zhaDevice.nwk)}</div>
|
<div>Nwk: ${formatAsPaddedHex(this._zhaDevice.nwk)}</div>
|
||||||
<div>Device Type: ${this._zhaDevice.device_type}</div>
|
<div>Device Type: ${this._zhaDevice.device_type}</div>
|
||||||
<div>
|
<div>
|
||||||
LQI:
|
LQI:
|
||||||
${this._zhaDevice.lqi ||
|
${this._zhaDevice.lqi ||
|
||||||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
|
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
RSSI:
|
RSSI:
|
||||||
${this._zhaDevice.rssi ||
|
${this._zhaDevice.rssi ||
|
||||||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
|
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
${this.hass!.localize("ui.dialogs.zha_device_info.last_seen")}:
|
${this.hass!.localize("ui.dialogs.zha_device_info.last_seen")}:
|
||||||
${this._zhaDevice.last_seen ||
|
${this._zhaDevice.last_seen ||
|
||||||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
|
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
${this.hass!.localize("ui.dialogs.zha_device_info.power_source")}:
|
${this.hass!.localize("ui.dialogs.zha_device_info.power_source")}:
|
||||||
${this._zhaDevice.power_source ||
|
${this._zhaDevice.power_source ||
|
||||||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
|
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
|
||||||
</div>
|
</div>
|
||||||
${this._zhaDevice.quirk_applied
|
${this._zhaDevice.quirk_applied
|
||||||
? html`
|
? html`
|
||||||
<div>
|
<div>
|
||||||
${this.hass!.localize("ui.dialogs.zha_device_info.quirk")}:
|
${this.hass!.localize("ui.dialogs.zha_device_info.quirk")}:
|
||||||
${this._zhaDevice.quirk_class}
|
${this._zhaDevice.quirk_class}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
|
</ha-expansion-panel>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +88,11 @@ export class HaDeviceActionsZha extends LitElement {
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
ha-expansion-panel {
|
||||||
|
--expansion-panel-summary-padding: 0;
|
||||||
|
--expansion-panel-content-padding: 0;
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,68 @@
|
|||||||
|
import { getConfigEntries } from "../../../../../../data/config_entries";
|
||||||
|
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
||||||
|
import { fetchZwaveNodeStatus } from "../../../../../../data/zwave_js";
|
||||||
|
import type { HomeAssistant } from "../../../../../../types";
|
||||||
|
import { showZWaveJSHealNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-heal-node";
|
||||||
|
import { showZWaveJSReinterviewNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-reinterview-node";
|
||||||
|
import { showZWaveJSRemoveFailedNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-failed-node";
|
||||||
|
import type { DeviceAction } from "../../../ha-config-device-page";
|
||||||
|
|
||||||
|
export const getZwaveDeviceActions = async (
|
||||||
|
el: HTMLElement,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
device: DeviceRegistryEntry
|
||||||
|
): Promise<DeviceAction[]> => {
|
||||||
|
const configEntries = await getConfigEntries(hass, {
|
||||||
|
domain: "zwave_js",
|
||||||
|
});
|
||||||
|
|
||||||
|
const configEntry = configEntries.find((entry) =>
|
||||||
|
device.config_entries.includes(entry.entry_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!configEntry) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryId = configEntry.entry_id;
|
||||||
|
|
||||||
|
const node = await fetchZwaveNodeStatus(hass, device.id);
|
||||||
|
|
||||||
|
if (!node || node.is_controller_node) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.device_info.device_config"
|
||||||
|
),
|
||||||
|
href: `/config/zwave_js/node_config/${device.id}?config_entry=${entryId}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.device_info.reinterview_device"
|
||||||
|
),
|
||||||
|
action: () =>
|
||||||
|
showZWaveJSReinterviewNodeDialog(el, {
|
||||||
|
device_id: device.id,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: hass.localize("ui.panel.config.zwave_js.device_info.heal_node"),
|
||||||
|
action: () =>
|
||||||
|
showZWaveJSHealNodeDialog(el, {
|
||||||
|
device: device,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.device_info.remove_failed"
|
||||||
|
),
|
||||||
|
action: () =>
|
||||||
|
showZWaveJSRemoveFailedNodeDialog(el, {
|
||||||
|
device_id: device.id,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
@@ -1,139 +0,0 @@
|
|||||||
import "@material/mwc-button/mwc-button";
|
|
||||||
import {
|
|
||||||
css,
|
|
||||||
CSSResultGroup,
|
|
||||||
html,
|
|
||||||
LitElement,
|
|
||||||
PropertyValues,
|
|
||||||
TemplateResult,
|
|
||||||
} from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
|
||||||
import {
|
|
||||||
fetchZwaveNodeStatus,
|
|
||||||
getZwaveJsIdentifiersFromDevice,
|
|
||||||
ZWaveJSNodeIdentifiers,
|
|
||||||
ZWaveJSNodeStatus,
|
|
||||||
} from "../../../../../../data/zwave_js";
|
|
||||||
import { haStyle } from "../../../../../../resources/styles";
|
|
||||||
import { HomeAssistant } from "../../../../../../types";
|
|
||||||
import { showZWaveJSReinterviewNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-reinterview-node";
|
|
||||||
import { showZWaveJSHealNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-heal-node";
|
|
||||||
import { showZWaveJSRemoveFailedNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-failed-node";
|
|
||||||
|
|
||||||
@customElement("ha-device-actions-zwave_js")
|
|
||||||
export class HaDeviceActionsZWaveJS extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property() public device!: DeviceRegistryEntry;
|
|
||||||
|
|
||||||
@state() private _entryId?: string;
|
|
||||||
|
|
||||||
@state() private _nodeId?: number;
|
|
||||||
|
|
||||||
@state() private _node?: ZWaveJSNodeStatus;
|
|
||||||
|
|
||||||
protected updated(changedProperties: PropertyValues) {
|
|
||||||
if (changedProperties.has("device")) {
|
|
||||||
const identifiers: ZWaveJSNodeIdentifiers | undefined =
|
|
||||||
getZwaveJsIdentifiersFromDevice(this.device);
|
|
||||||
if (!identifiers) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._nodeId = identifiers.node_id;
|
|
||||||
this._entryId = this.device.config_entries[0];
|
|
||||||
|
|
||||||
this._fetchNodeDetails();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async _fetchNodeDetails() {
|
|
||||||
if (!this._nodeId || !this._entryId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._node = await fetchZwaveNodeStatus(
|
|
||||||
this.hass,
|
|
||||||
this._entryId,
|
|
||||||
this._nodeId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
if (!this._node) {
|
|
||||||
return html``;
|
|
||||||
}
|
|
||||||
return html`
|
|
||||||
${!this._node.is_controller_node
|
|
||||||
? html`
|
|
||||||
<a
|
|
||||||
.href=${`/config/zwave_js/node_config/${this.device.id}?config_entry=${this._entryId}`}
|
|
||||||
>
|
|
||||||
<mwc-button>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.zwave_js.device_info.device_config"
|
|
||||||
)}
|
|
||||||
</mwc-button>
|
|
||||||
</a>
|
|
||||||
<mwc-button @click=${this._reinterviewClicked}>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.zwave_js.device_info.reinterview_device"
|
|
||||||
)}
|
|
||||||
</mwc-button>
|
|
||||||
<mwc-button @click=${this._healNodeClicked}>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.zwave_js.device_info.heal_node"
|
|
||||||
)}
|
|
||||||
</mwc-button>
|
|
||||||
<mwc-button @click=${this._removeFailedNode}>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.zwave_js.device_info.remove_failed"
|
|
||||||
)}
|
|
||||||
</mwc-button>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _reinterviewClicked() {
|
|
||||||
if (!this._nodeId || !this._entryId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showZWaveJSReinterviewNodeDialog(this, {
|
|
||||||
entry_id: this._entryId,
|
|
||||||
node_id: this._nodeId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _healNodeClicked() {
|
|
||||||
if (!this._nodeId || !this._entryId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showZWaveJSHealNodeDialog(this, {
|
|
||||||
entry_id: this._entryId,
|
|
||||||
node_id: this._nodeId,
|
|
||||||
device: this.device,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _removeFailedNode() {
|
|
||||||
if (!this._nodeId || !this._entryId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showZWaveJSRemoveFailedNodeDialog(this, {
|
|
||||||
entry_id: this._entryId,
|
|
||||||
node_id: this._nodeId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return [
|
|
||||||
haStyle,
|
|
||||||
css`
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,52 @@
|
|||||||
|
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
||||||
|
import {
|
||||||
|
ZwaveJSNodeComments,
|
||||||
|
fetchZwaveNodeComments,
|
||||||
|
} from "../../../../../../data/zwave_js";
|
||||||
|
import { HomeAssistant } from "../../../../../../types";
|
||||||
|
|
||||||
|
@customElement("ha-device-alerts-zwave_js")
|
||||||
|
export class HaDeviceAlertsZWaveJS extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public device!: DeviceRegistryEntry;
|
||||||
|
|
||||||
|
@state() private _nodeComments?: ZwaveJSNodeComments;
|
||||||
|
|
||||||
|
protected willUpdate(changedProperties: PropertyValues) {
|
||||||
|
super.willUpdate(changedProperties);
|
||||||
|
if (changedProperties.has("device")) {
|
||||||
|
this._fetchNodeDetails();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchNodeDetails() {
|
||||||
|
this._nodeComments = await fetchZwaveNodeComments(
|
||||||
|
this.hass,
|
||||||
|
this.device.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (this._nodeComments && this._nodeComments.comments?.length > 0) {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
${this._nodeComments.comments.map(
|
||||||
|
(comment) => html`<ha-alert .alertType=${comment.level}>
|
||||||
|
${comment.text}
|
||||||
|
</ha-alert>`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-device-alerts-zwave_js": HaDeviceAlertsZWaveJS;
|
||||||
|
}
|
||||||
|
}
|
@@ -7,18 +7,17 @@ import {
|
|||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
import "../../../../../../components/ha-expansion-panel";
|
||||||
import {
|
import {
|
||||||
ConfigEntry,
|
ConfigEntry,
|
||||||
getConfigEntries,
|
getConfigEntries,
|
||||||
} from "../../../../../../data/config_entries";
|
} from "../../../../../../data/config_entries";
|
||||||
|
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
||||||
import {
|
import {
|
||||||
fetchZwaveNodeStatus,
|
fetchZwaveNodeStatus,
|
||||||
getZwaveJsIdentifiersFromDevice,
|
|
||||||
nodeStatus,
|
nodeStatus,
|
||||||
ZWaveJSNodeStatus,
|
|
||||||
ZWaveJSNodeIdentifiers,
|
|
||||||
SecurityClass,
|
SecurityClass,
|
||||||
|
ZWaveJSNodeStatus,
|
||||||
} from "../../../../../../data/zwave_js";
|
} from "../../../../../../data/zwave_js";
|
||||||
import { haStyle } from "../../../../../../resources/styles";
|
import { haStyle } from "../../../../../../resources/styles";
|
||||||
import { HomeAssistant } from "../../../../../../types";
|
import { HomeAssistant } from "../../../../../../types";
|
||||||
@@ -29,57 +28,41 @@ export class HaDeviceInfoZWaveJS extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public device!: DeviceRegistryEntry;
|
@property({ attribute: false }) public device!: DeviceRegistryEntry;
|
||||||
|
|
||||||
@state() private _entryId?: string;
|
|
||||||
|
|
||||||
@state() private _configEntry?: ConfigEntry;
|
@state() private _configEntry?: ConfigEntry;
|
||||||
|
|
||||||
@state() private _multipleConfigEntries = false;
|
@state() private _multipleConfigEntries = false;
|
||||||
|
|
||||||
@state() private _nodeId?: number;
|
|
||||||
|
|
||||||
@state() private _node?: ZWaveJSNodeStatus;
|
@state() private _node?: ZWaveJSNodeStatus;
|
||||||
|
|
||||||
protected updated(changedProperties: PropertyValues) {
|
public willUpdate(changedProperties: PropertyValues) {
|
||||||
|
super.willUpdate(changedProperties);
|
||||||
if (changedProperties.has("device")) {
|
if (changedProperties.has("device")) {
|
||||||
const identifiers: ZWaveJSNodeIdentifiers | undefined =
|
|
||||||
getZwaveJsIdentifiersFromDevice(this.device);
|
|
||||||
if (!identifiers) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._nodeId = identifiers.node_id;
|
|
||||||
this._entryId = this.device.config_entries[0];
|
|
||||||
|
|
||||||
this._fetchNodeDetails();
|
this._fetchNodeDetails();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _fetchNodeDetails() {
|
protected async _fetchNodeDetails() {
|
||||||
if (!this._nodeId || !this._entryId) {
|
if (!this.device) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const configEntries = await getConfigEntries(this.hass, {
|
const configEntries = await getConfigEntries(this.hass, {
|
||||||
domain: "zwave_js",
|
domain: "zwave_js",
|
||||||
});
|
});
|
||||||
let zwaveJsConfEntries = 0;
|
|
||||||
for (const entry of configEntries) {
|
this._multipleConfigEntries = configEntries.length > 1;
|
||||||
if (zwaveJsConfEntries) {
|
|
||||||
this._multipleConfigEntries = true;
|
const configEntry = configEntries.find((entry) =>
|
||||||
}
|
this.device.config_entries.includes(entry.entry_id)
|
||||||
if (entry.entry_id === this._entryId) {
|
);
|
||||||
this._configEntry = entry;
|
|
||||||
}
|
if (!configEntry) {
|
||||||
if (this._configEntry && this._multipleConfigEntries) {
|
return;
|
||||||
break;
|
|
||||||
}
|
|
||||||
zwaveJsConfEntries++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._node = await fetchZwaveNodeStatus(
|
this._configEntry = configEntry;
|
||||||
this.hass,
|
|
||||||
this._entryId,
|
this._node = await fetchZwaveNodeStatus(this.hass, this.device.id);
|
||||||
this._nodeId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
@@ -87,73 +70,76 @@ export class HaDeviceInfoZWaveJS extends LitElement {
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<h4>
|
<ha-expansion-panel
|
||||||
${this.hass.localize("ui.panel.config.zwave_js.device_info.zwave_info")}
|
.header=${this.hass.localize(
|
||||||
</h4>
|
"ui.panel.config.zwave_js.device_info.zwave_info"
|
||||||
${this._multipleConfigEntries
|
)}
|
||||||
? html`
|
>
|
||||||
<div>
|
${this._multipleConfigEntries
|
||||||
${this.hass.localize("ui.panel.config.zwave_js.common.source")}:
|
? html`
|
||||||
${this._configEntry!.title}
|
<div>
|
||||||
</div>
|
${this.hass.localize("ui.panel.config.zwave_js.common.source")}:
|
||||||
`
|
${this._configEntry!.title}
|
||||||
: ""}
|
</div>
|
||||||
<div>
|
`
|
||||||
${this.hass.localize("ui.panel.config.zwave_js.common.node_id")}:
|
: ""}
|
||||||
${this._node.node_id}
|
<div>
|
||||||
</div>
|
${this.hass.localize("ui.panel.config.zwave_js.common.node_id")}:
|
||||||
${!this._node.is_controller_node
|
${this._node.node_id}
|
||||||
? html`
|
</div>
|
||||||
<div>
|
${!this._node.is_controller_node
|
||||||
${this.hass.localize(
|
? html`
|
||||||
"ui.panel.config.zwave_js.device_info.node_status"
|
<div>
|
||||||
)}:
|
${this.hass.localize(
|
||||||
${this.hass.localize(
|
"ui.panel.config.zwave_js.device_info.node_status"
|
||||||
`ui.panel.config.zwave_js.node_status.${
|
)}:
|
||||||
nodeStatus[this._node.status]
|
${this.hass.localize(
|
||||||
}`
|
`ui.panel.config.zwave_js.node_status.${
|
||||||
)}
|
nodeStatus[this._node.status]
|
||||||
</div>
|
}`
|
||||||
<div>
|
)}
|
||||||
${this.hass.localize(
|
</div>
|
||||||
"ui.panel.config.zwave_js.device_info.node_ready"
|
<div>
|
||||||
)}:
|
${this.hass.localize(
|
||||||
${this._node.ready
|
"ui.panel.config.zwave_js.device_info.node_ready"
|
||||||
? this.hass.localize("ui.common.yes")
|
)}:
|
||||||
: this.hass.localize("ui.common.no")}
|
${this._node.ready
|
||||||
</div>
|
? this.hass.localize("ui.common.yes")
|
||||||
<div>
|
: this.hass.localize("ui.common.no")}
|
||||||
${this.hass.localize(
|
</div>
|
||||||
"ui.panel.config.zwave_js.device_info.highest_security"
|
<div>
|
||||||
)}:
|
${this.hass.localize(
|
||||||
${this._node.highest_security_class !== null
|
"ui.panel.config.zwave_js.device_info.highest_security"
|
||||||
? this.hass.localize(
|
)}:
|
||||||
`ui.panel.config.zwave_js.security_classes.${
|
${this._node.highest_security_class !== null
|
||||||
SecurityClass[this._node.highest_security_class]
|
? this.hass.localize(
|
||||||
}.title`
|
`ui.panel.config.zwave_js.security_classes.${
|
||||||
)
|
SecurityClass[this._node.highest_security_class]
|
||||||
: this._node.is_secure === false
|
}.title`
|
||||||
? this.hass.localize(
|
)
|
||||||
"ui.panel.config.zwave_js.security_classes.none.title"
|
: this._node.is_secure === false
|
||||||
)
|
? this.hass.localize(
|
||||||
: this.hass.localize(
|
"ui.panel.config.zwave_js.security_classes.none.title"
|
||||||
"ui.panel.config.zwave_js.device_info.unknown"
|
)
|
||||||
)}
|
: this.hass.localize(
|
||||||
</div>
|
"ui.panel.config.zwave_js.device_info.unknown"
|
||||||
<div>
|
)}
|
||||||
${this.hass.localize(
|
</div>
|
||||||
"ui.panel.config.zwave_js.device_info.zwave_plus"
|
<div>
|
||||||
)}:
|
${this.hass.localize(
|
||||||
${this._node.zwave_plus_version
|
"ui.panel.config.zwave_js.device_info.zwave_plus"
|
||||||
? this.hass.localize(
|
)}:
|
||||||
"ui.panel.config.zwave_js.device_info.zwave_plus_version",
|
${this._node.zwave_plus_version
|
||||||
"version",
|
? this.hass.localize(
|
||||||
this._node.zwave_plus_version
|
"ui.panel.config.zwave_js.device_info.zwave_plus_version",
|
||||||
)
|
"version",
|
||||||
: this.hass.localize("ui.common.no")}
|
this._node.zwave_plus_version
|
||||||
</div>
|
)
|
||||||
`
|
: this.hass.localize("ui.common.no")}
|
||||||
: ""}
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</ha-expansion-panel>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +154,11 @@ export class HaDeviceInfoZWaveJS extends LitElement {
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
ha-expansion-panel {
|
||||||
|
--expansion-panel-summary-padding: 0;
|
||||||
|
--expansion-panel-content-padding: 0;
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,9 @@
|
|||||||
import { mdiOpenInNew, mdiPencil, mdiPlusCircle } from "@mdi/js";
|
import {
|
||||||
|
mdiDotsVertical,
|
||||||
|
mdiOpenInNew,
|
||||||
|
mdiPencil,
|
||||||
|
mdiPlusCircle,
|
||||||
|
} from "@mdi/js";
|
||||||
import "@polymer/paper-tooltip/paper-tooltip";
|
import "@polymer/paper-tooltip/paper-tooltip";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
@@ -13,6 +18,7 @@ import { slugify } from "../../../common/string/slugify";
|
|||||||
import { groupBy } from "../../../common/util/group-by";
|
import { groupBy } from "../../../common/util/group-by";
|
||||||
import "../../../components/entity/ha-battery-icon";
|
import "../../../components/entity/ha-battery-icon";
|
||||||
import "../../../components/ha-alert";
|
import "../../../components/ha-alert";
|
||||||
|
import "../../../components/ha-button-menu";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
import "../../../components/ha-icon-next";
|
import "../../../components/ha-icon-next";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
@@ -26,14 +32,14 @@ import {
|
|||||||
import {
|
import {
|
||||||
computeDeviceName,
|
computeDeviceName,
|
||||||
DeviceRegistryEntry,
|
DeviceRegistryEntry,
|
||||||
updateDeviceRegistryEntry,
|
|
||||||
removeConfigEntryFromDevice,
|
removeConfigEntryFromDevice,
|
||||||
|
updateDeviceRegistryEntry,
|
||||||
} from "../../../data/device_registry";
|
} from "../../../data/device_registry";
|
||||||
import {
|
import {
|
||||||
fetchDiagnosticHandler,
|
|
||||||
getDeviceDiagnosticsDownloadUrl,
|
|
||||||
getConfigEntryDiagnosticsDownloadUrl,
|
|
||||||
DiagnosticInfo,
|
DiagnosticInfo,
|
||||||
|
fetchDiagnosticHandler,
|
||||||
|
getConfigEntryDiagnosticsDownloadUrl,
|
||||||
|
getDeviceDiagnosticsDownloadUrl,
|
||||||
} from "../../../data/diagnostics";
|
} from "../../../data/diagnostics";
|
||||||
import {
|
import {
|
||||||
EntityRegistryEntry,
|
EntityRegistryEntry,
|
||||||
@@ -51,9 +57,10 @@ import {
|
|||||||
import "../../../layouts/hass-error-screen";
|
import "../../../layouts/hass-error-screen";
|
||||||
import "../../../layouts/hass-tabs-subpage";
|
import "../../../layouts/hass-tabs-subpage";
|
||||||
import { haStyle } from "../../../resources/styles";
|
import { haStyle } from "../../../resources/styles";
|
||||||
import { HomeAssistant, Route } from "../../../types";
|
import type { HomeAssistant, Route } from "../../../types";
|
||||||
import { brandsUrl } from "../../../util/brands-url";
|
import { brandsUrl } from "../../../util/brands-url";
|
||||||
import { fileDownload } from "../../../util/file_download";
|
import { fileDownload } from "../../../util/file_download";
|
||||||
|
import "../../logbook/ha-logbook";
|
||||||
import "../ha-config-section";
|
import "../ha-config-section";
|
||||||
import { configSections } from "../ha-panel-config";
|
import { configSections } from "../ha-panel-config";
|
||||||
import "./device-detail/ha-device-entities-card";
|
import "./device-detail/ha-device-entities-card";
|
||||||
@@ -68,6 +75,14 @@ export interface EntityRegistryStateEntry extends EntityRegistryEntry {
|
|||||||
stateName?: string | null;
|
stateName?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DeviceAction {
|
||||||
|
href?: string;
|
||||||
|
action?: (ev: any) => void;
|
||||||
|
label: string;
|
||||||
|
trailingIcon?: string;
|
||||||
|
classes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@customElement("ha-config-device-page")
|
@customElement("ha-config-device-page")
|
||||||
export class HaConfigDevicePage extends LitElement {
|
export class HaConfigDevicePage extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -93,11 +108,13 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
@state() private _related?: RelatedResult;
|
@state() private _related?: RelatedResult;
|
||||||
|
|
||||||
// If a number, it's the request ID so we make sure we don't show older info
|
// If a number, it's the request ID so we make sure we don't show older info
|
||||||
@state() private _diagnosticDownloadLinks?:
|
@state() private _diagnosticDownloadLinks?: number | DeviceAction[];
|
||||||
| number
|
|
||||||
| (TemplateResult | string)[];
|
|
||||||
|
|
||||||
@state() private _deleteButtons?: (TemplateResult | string)[];
|
@state() private _deleteButtons?: DeviceAction[];
|
||||||
|
|
||||||
|
@state() private _deviceActions?: DeviceAction[];
|
||||||
|
|
||||||
|
private _logbookTime = { recent: 86400 };
|
||||||
|
|
||||||
private _device = memoizeOne(
|
private _device = memoizeOne(
|
||||||
(
|
(
|
||||||
@@ -131,6 +148,13 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private _deviceIdInList = memoizeOne((deviceId: string) => [deviceId]);
|
||||||
|
|
||||||
|
private _entityIds = memoizeOne(
|
||||||
|
(entries: EntityRegistryStateEntry[]): string[] =>
|
||||||
|
entries.map((entry) => entry.entity_id)
|
||||||
|
);
|
||||||
|
|
||||||
private _entitiesByCategory = memoizeOne(
|
private _entitiesByCategory = memoizeOne(
|
||||||
(entities: EntityRegistryEntry[]) => {
|
(entities: EntityRegistryEntry[]) => {
|
||||||
const result = groupBy(entities, (entry) =>
|
const result = groupBy(entities, (entry) =>
|
||||||
@@ -186,15 +210,17 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
if (
|
if (
|
||||||
changedProps.has("deviceId") ||
|
changedProps.has("deviceId") ||
|
||||||
changedProps.has("devices") ||
|
changedProps.has("devices") ||
|
||||||
changedProps.has("deviceId") ||
|
|
||||||
changedProps.has("entries")
|
changedProps.has("entries")
|
||||||
) {
|
) {
|
||||||
this._diagnosticDownloadLinks = undefined;
|
this._diagnosticDownloadLinks = undefined;
|
||||||
this._deleteButtons = undefined;
|
this._deleteButtons = undefined;
|
||||||
|
this._deviceActions = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(this._diagnosticDownloadLinks && this._deleteButtons) ||
|
(this._diagnosticDownloadLinks &&
|
||||||
|
this._deleteButtons &&
|
||||||
|
this._deviceActions) ||
|
||||||
!this.devices ||
|
!this.devices ||
|
||||||
!this.deviceId ||
|
!this.deviceId ||
|
||||||
!this.entries
|
!this.entries
|
||||||
@@ -204,129 +230,10 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
|
|
||||||
this._diagnosticDownloadLinks = Math.random();
|
this._diagnosticDownloadLinks = Math.random();
|
||||||
this._deleteButtons = []; // To prevent re-rendering if no delete buttons
|
this._deleteButtons = []; // To prevent re-rendering if no delete buttons
|
||||||
this._renderDiagnosticButtons(this._diagnosticDownloadLinks);
|
this._deviceActions = [];
|
||||||
this._renderDeleteButtons();
|
this._getDiagnosticButtons(this._diagnosticDownloadLinks);
|
||||||
}
|
this._getDeleteActions();
|
||||||
|
this._getDeviceActions();
|
||||||
private async _renderDiagnosticButtons(requestId: number): Promise<void> {
|
|
||||||
if (!isComponentLoaded(this.hass, "diagnostics")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const device = this._device(this.deviceId, this.devices);
|
|
||||||
|
|
||||||
if (!device) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let links = await Promise.all(
|
|
||||||
this._integrations(device, this.entries).map(
|
|
||||||
async (entry): Promise<boolean | { link: string; domain: string }> => {
|
|
||||||
if (entry.state !== "loaded") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let info: DiagnosticInfo;
|
|
||||||
try {
|
|
||||||
info = await fetchDiagnosticHandler(this.hass, entry.domain);
|
|
||||||
} catch (err: any) {
|
|
||||||
if (err.code === "not_found") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!info.handlers.device && !info.handlers.config_entry) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
link: info.handlers.device
|
|
||||||
? getDeviceDiagnosticsDownloadUrl(entry.entry_id, this.deviceId)
|
|
||||||
: getConfigEntryDiagnosticsDownloadUrl(entry.entry_id),
|
|
||||||
domain: entry.domain,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
links = links.filter(Boolean);
|
|
||||||
|
|
||||||
if (this._diagnosticDownloadLinks !== requestId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (links.length > 0) {
|
|
||||||
this._diagnosticDownloadLinks = (
|
|
||||||
links as { link: string; domain: string }[]
|
|
||||||
).map(
|
|
||||||
(link) => html`
|
|
||||||
<a href=${link.link} @click=${this._signUrl}>
|
|
||||||
<mwc-button>
|
|
||||||
${links.length > 1
|
|
||||||
? this.hass.localize(
|
|
||||||
`ui.panel.config.devices.download_diagnostics_integration`,
|
|
||||||
{
|
|
||||||
integration: domainToName(
|
|
||||||
this.hass.localize,
|
|
||||||
link.domain
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
: this.hass.localize(
|
|
||||||
`ui.panel.config.devices.download_diagnostics`
|
|
||||||
)}
|
|
||||||
</mwc-button>
|
|
||||||
</a>
|
|
||||||
`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderDeleteButtons() {
|
|
||||||
const device = this._device(this.deviceId, this.devices);
|
|
||||||
|
|
||||||
if (!device) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttons: TemplateResult[] = [];
|
|
||||||
this._integrations(device, this.entries).forEach((entry) => {
|
|
||||||
if (entry.state !== "loaded" || !entry.supports_remove_device) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
buttons.push(html`
|
|
||||||
<mwc-button
|
|
||||||
class="warning"
|
|
||||||
.entryId=${entry.entry_id}
|
|
||||||
@click=${this._confirmDeleteEntry}
|
|
||||||
>
|
|
||||||
${buttons.length > 1
|
|
||||||
? this.hass.localize(
|
|
||||||
`ui.panel.config.devices.delete_device_integration`,
|
|
||||||
{
|
|
||||||
integration: domainToName(this.hass.localize, entry.domain),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
: this.hass.localize(`ui.panel.config.devices.delete_device`)}
|
|
||||||
</mwc-button>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (buttons.length > 0) {
|
|
||||||
this._deleteButtons = buttons;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _confirmDeleteEntry(e: MouseEvent): Promise<void> {
|
|
||||||
const entryId = (e.currentTarget as any).entryId;
|
|
||||||
|
|
||||||
const confirmed = await showConfirmationDialog(this, {
|
|
||||||
text: this.hass.localize("ui.panel.config.devices.confirm_delete"),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await removeConfigEntryFromDevice(this.hass!, this.deviceId, entryId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated(changedProps) {
|
protected firstUpdated(changedProps) {
|
||||||
@@ -371,14 +278,18 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
: undefined;
|
: undefined;
|
||||||
const area = this._computeArea(this.areas, device);
|
const area = this._computeArea(this.areas, device);
|
||||||
|
|
||||||
const configurationUrlIsHomeAssistant =
|
|
||||||
device.configuration_url?.startsWith("homeassistant://") || false;
|
|
||||||
|
|
||||||
const configurationUrl = configurationUrlIsHomeAssistant
|
|
||||||
? device.configuration_url!.replace("homeassistant://", "/")
|
|
||||||
: device.configuration_url;
|
|
||||||
|
|
||||||
const deviceInfo: TemplateResult[] = [];
|
const deviceInfo: TemplateResult[] = [];
|
||||||
|
const deviceAlerts: TemplateResult[] = [];
|
||||||
|
|
||||||
|
const actions = [...(this._deviceActions || [])];
|
||||||
|
if (Array.isArray(this._diagnosticDownloadLinks)) {
|
||||||
|
actions.push(...this._diagnosticDownloadLinks);
|
||||||
|
}
|
||||||
|
if (this._deleteButtons) {
|
||||||
|
actions.push(...this._deleteButtons);
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstDeviceAction = actions.shift();
|
||||||
|
|
||||||
if (device.disabled_by) {
|
if (device.disabled_by) {
|
||||||
deviceInfo.push(
|
deviceInfo.push(
|
||||||
@@ -397,53 +308,19 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
)}
|
)}
|
||||||
</ha-alert>
|
</ha-alert>
|
||||||
${device.disabled_by === "user"
|
${device.disabled_by === "user"
|
||||||
? html` <div class="card-actions" slot="actions">
|
? html`
|
||||||
<mwc-button unelevated @click=${this._enableDevice}>
|
<div class="card-actions" slot="actions">
|
||||||
${this.hass.localize("ui.common.enable")}
|
<mwc-button unelevated @click=${this._enableDevice}>
|
||||||
</mwc-button>
|
${this.hass.localize("ui.common.enable")}
|
||||||
</div>`
|
</mwc-button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
: ""}
|
: ""}
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const deviceActions: (TemplateResult | string)[] = [];
|
this._renderIntegrationInfo(device, integrations, deviceInfo, deviceAlerts);
|
||||||
|
|
||||||
if (configurationUrl) {
|
|
||||||
deviceActions.push(html`
|
|
||||||
<a
|
|
||||||
href=${configurationUrl}
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
.target=${configurationUrlIsHomeAssistant ? "_self" : "_blank"}
|
|
||||||
>
|
|
||||||
<mwc-button>
|
|
||||||
${this.hass.localize(
|
|
||||||
`ui.panel.config.devices.open_configuration_url_${
|
|
||||||
device.entry_type || "device"
|
|
||||||
}`
|
|
||||||
)}
|
|
||||||
<ha-svg-icon
|
|
||||||
.path=${mdiOpenInNew}
|
|
||||||
slot="trailingIcon"
|
|
||||||
></ha-svg-icon>
|
|
||||||
</mwc-button>
|
|
||||||
</a>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._renderIntegrationInfo(
|
|
||||||
device,
|
|
||||||
integrations,
|
|
||||||
deviceInfo,
|
|
||||||
deviceActions
|
|
||||||
);
|
|
||||||
|
|
||||||
if (Array.isArray(this._diagnosticDownloadLinks)) {
|
|
||||||
deviceActions.push(...this._diagnosticDownloadLinks);
|
|
||||||
}
|
|
||||||
if (this._deleteButtons) {
|
|
||||||
deviceActions.push(...this._deleteButtons);
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<hass-tabs-subpage
|
<hass-tabs-subpage
|
||||||
@@ -467,10 +344,6 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
`
|
`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header fullwidth">
|
<div class="header fullwidth">
|
||||||
${
|
${
|
||||||
@@ -537,6 +410,11 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
|
${
|
||||||
|
deviceAlerts.length
|
||||||
|
? html` <div class="fullwidth">${deviceAlerts}</div> `
|
||||||
|
: ""
|
||||||
|
}
|
||||||
<ha-device-info-card
|
<ha-device-info-card
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.areas=${this.areas}
|
.areas=${this.areas}
|
||||||
@@ -545,37 +423,70 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
>
|
>
|
||||||
${deviceInfo}
|
${deviceInfo}
|
||||||
${
|
${
|
||||||
deviceActions.length
|
firstDeviceAction || actions.length
|
||||||
? html`
|
? html`
|
||||||
<div class="card-actions" slot="actions">
|
<div class="card-actions" slot="actions">
|
||||||
${deviceActions}
|
<div>
|
||||||
|
<a href=${ifDefined(firstDeviceAction!.href)}>
|
||||||
|
<mwc-button
|
||||||
|
class=${ifDefined(firstDeviceAction!.classes)}
|
||||||
|
.action=${firstDeviceAction!.action}
|
||||||
|
@click=${this._deviceActionClicked}
|
||||||
|
>
|
||||||
|
${firstDeviceAction!.label}
|
||||||
|
${firstDeviceAction!.trailingIcon
|
||||||
|
? html`
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${firstDeviceAction!.trailingIcon}
|
||||||
|
slot="trailingIcon"
|
||||||
|
></ha-svg-icon>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</mwc-button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${actions.length
|
||||||
|
? html`
|
||||||
|
<ha-button-menu corner="BOTTOM_START">
|
||||||
|
<ha-icon-button
|
||||||
|
slot="trigger"
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.common.menu"
|
||||||
|
)}
|
||||||
|
.path=${mdiDotsVertical}
|
||||||
|
></ha-icon-button>
|
||||||
|
${actions.map(
|
||||||
|
(deviceAction) => html`
|
||||||
|
<a href=${ifDefined(deviceAction.href)}>
|
||||||
|
<mwc-list-item
|
||||||
|
class=${ifDefined(
|
||||||
|
deviceAction.classes
|
||||||
|
)}
|
||||||
|
.action=${deviceAction.action}
|
||||||
|
@click=${this._deviceActionClicked}
|
||||||
|
>
|
||||||
|
${deviceAction.label}
|
||||||
|
${deviceAction.trailingIcon
|
||||||
|
? html`
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${deviceAction.trailingIcon}
|
||||||
|
></ha-svg-icon>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</mwc-list-item>
|
||||||
|
</a>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ha-button-menu>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
</ha-device-info-card>
|
</ha-device-info-card>
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
${["control", "sensor", "config", "diagnostic"].map((category) =>
|
|
||||||
// Make sure we render controls if no other cards will be rendered
|
|
||||||
entitiesByCategory[category].length > 0 ||
|
|
||||||
(entities.length === 0 && category === "control")
|
|
||||||
? html`
|
|
||||||
<ha-device-entities-card
|
|
||||||
.hass=${this.hass}
|
|
||||||
.header=${this.hass.localize(
|
|
||||||
`ui.panel.config.devices.entities.${category}`
|
|
||||||
)}
|
|
||||||
.deviceName=${deviceName}
|
|
||||||
.entities=${entitiesByCategory[category]}
|
|
||||||
.showHidden=${device.disabled_by !== null}
|
|
||||||
>
|
|
||||||
</ha-device-entities-card>
|
|
||||||
`
|
|
||||||
: ""
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
${
|
${
|
||||||
isComponentLoaded(this.hass, "automation")
|
isComponentLoaded(this.hass, "automation")
|
||||||
? html`
|
? html`
|
||||||
@@ -844,12 +755,227 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
`
|
`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
${["control", "sensor", "config", "diagnostic"].map((category) =>
|
||||||
|
// Make sure we render controls if no other cards will be rendered
|
||||||
|
entitiesByCategory[category].length > 0 ||
|
||||||
|
(entities.length === 0 && category === "control")
|
||||||
|
? html`
|
||||||
|
<ha-device-entities-card
|
||||||
|
.hass=${this.hass}
|
||||||
|
.header=${this.hass.localize(
|
||||||
|
`ui.panel.config.devices.entities.${category}`
|
||||||
|
)}
|
||||||
|
.deviceName=${deviceName}
|
||||||
|
.entities=${entitiesByCategory[category]}
|
||||||
|
.showHidden=${device.disabled_by !== null}
|
||||||
|
>
|
||||||
|
</ha-device-entities-card>
|
||||||
|
`
|
||||||
|
: ""
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
${
|
||||||
|
isComponentLoaded(this.hass, "logbook")
|
||||||
|
? html`
|
||||||
|
<ha-card outlined>
|
||||||
|
<h1 class="card-header">
|
||||||
|
${this.hass.localize("panel.logbook")}
|
||||||
|
</h1>
|
||||||
|
<ha-logbook
|
||||||
|
.hass=${this.hass}
|
||||||
|
.time=${this._logbookTime}
|
||||||
|
.entityIds=${this._entityIds(entities)}
|
||||||
|
.deviceIds=${this._deviceIdInList(this.deviceId)}
|
||||||
|
virtualize
|
||||||
|
narrow
|
||||||
|
no-icon
|
||||||
|
></ha-logbook>
|
||||||
|
</ha-card>
|
||||||
|
`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ha-config-section>
|
</ha-config-section>
|
||||||
</hass-tabs-subpage> `;
|
</hass-tabs-subpage> `;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _getDiagnosticButtons(requestId: number): Promise<void> {
|
||||||
|
if (!isComponentLoaded(this.hass, "diagnostics")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const device = this._device(this.deviceId, this.devices);
|
||||||
|
|
||||||
|
if (!device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let links = await Promise.all(
|
||||||
|
this._integrations(device, this.entries).map(
|
||||||
|
async (entry): Promise<boolean | { link: string; domain: string }> => {
|
||||||
|
if (entry.state !== "loaded") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let info: DiagnosticInfo;
|
||||||
|
try {
|
||||||
|
info = await fetchDiagnosticHandler(this.hass, entry.domain);
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.code === "not_found") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!info.handlers.device && !info.handlers.config_entry) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
link: info.handlers.device
|
||||||
|
? getDeviceDiagnosticsDownloadUrl(entry.entry_id, this.deviceId)
|
||||||
|
: getConfigEntryDiagnosticsDownloadUrl(entry.entry_id),
|
||||||
|
domain: entry.domain,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
links = links.filter(Boolean);
|
||||||
|
|
||||||
|
if (this._diagnosticDownloadLinks !== requestId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (links.length > 0) {
|
||||||
|
this._diagnosticDownloadLinks = (
|
||||||
|
links as { link: string; domain: string }[]
|
||||||
|
).map((link) => ({
|
||||||
|
href: link.link,
|
||||||
|
action: (ev) => this._signUrl(ev),
|
||||||
|
label:
|
||||||
|
links.length > 1
|
||||||
|
? this.hass.localize(
|
||||||
|
`ui.panel.config.devices.download_diagnostics_integration`,
|
||||||
|
{
|
||||||
|
integration: domainToName(this.hass.localize, link.domain),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: this.hass.localize(
|
||||||
|
`ui.panel.config.devices.download_diagnostics`
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getDeleteActions() {
|
||||||
|
const device = this._device(this.deviceId, this.devices);
|
||||||
|
|
||||||
|
if (!device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttons: DeviceAction[] = [];
|
||||||
|
this._integrations(device, this.entries).forEach((entry) => {
|
||||||
|
if (entry.state !== "loaded" || !entry.supports_remove_device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
buttons.push({
|
||||||
|
action: async () => {
|
||||||
|
const confirmed = await showConfirmationDialog(this, {
|
||||||
|
text: this.hass.localize("ui.panel.config.devices.confirm_delete"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await removeConfigEntryFromDevice(
|
||||||
|
this.hass!,
|
||||||
|
this.deviceId,
|
||||||
|
entry.entry_id
|
||||||
|
);
|
||||||
|
},
|
||||||
|
classes: "warning",
|
||||||
|
label:
|
||||||
|
buttons.length > 1
|
||||||
|
? this.hass.localize(
|
||||||
|
`ui.panel.config.devices.delete_device_integration`,
|
||||||
|
{
|
||||||
|
integration: domainToName(this.hass.localize, entry.domain),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: this.hass.localize(`ui.panel.config.devices.delete_device`),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (buttons.length > 0) {
|
||||||
|
this._deleteButtons = buttons;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getDeviceActions() {
|
||||||
|
const device = this._device(this.deviceId, this.devices);
|
||||||
|
|
||||||
|
if (!device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceActions: DeviceAction[] = [];
|
||||||
|
|
||||||
|
const configurationUrlIsHomeAssistant =
|
||||||
|
device.configuration_url?.startsWith("homeassistant://") || false;
|
||||||
|
|
||||||
|
const configurationUrl = configurationUrlIsHomeAssistant
|
||||||
|
? device.configuration_url!.replace("homeassistant://", "/")
|
||||||
|
: device.configuration_url;
|
||||||
|
|
||||||
|
if (configurationUrl) {
|
||||||
|
deviceActions.push({
|
||||||
|
href: configurationUrl,
|
||||||
|
label: this.hass.localize(
|
||||||
|
`ui.panel.config.devices.open_configuration_url_${
|
||||||
|
device.entry_type || "device"
|
||||||
|
}`
|
||||||
|
),
|
||||||
|
trailingIcon: mdiOpenInNew,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const domains = this._integrations(device, this.entries).map(
|
||||||
|
(int) => int.domain
|
||||||
|
);
|
||||||
|
|
||||||
|
if (domains.includes("mqtt")) {
|
||||||
|
const mqtt = await import(
|
||||||
|
"./device-detail/integration-elements/mqtt/device-actions"
|
||||||
|
);
|
||||||
|
const actions = mqtt.getMQTTDeviceActions(this, device);
|
||||||
|
deviceActions.push(...actions);
|
||||||
|
}
|
||||||
|
if (domains.includes("zha")) {
|
||||||
|
const zha = await import(
|
||||||
|
"./device-detail/integration-elements/zha/device-actions"
|
||||||
|
);
|
||||||
|
const actions = await zha.getZHADeviceActions(this, this.hass, device);
|
||||||
|
deviceActions.push(...actions);
|
||||||
|
}
|
||||||
|
if (domains.includes("zwave_js")) {
|
||||||
|
const zwave = await import(
|
||||||
|
"./device-detail/integration-elements/zwave_js/device-actions"
|
||||||
|
);
|
||||||
|
const actions = await zwave.getZwaveDeviceActions(
|
||||||
|
this,
|
||||||
|
this.hass,
|
||||||
|
device
|
||||||
|
);
|
||||||
|
deviceActions.push(...actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._deviceActions = deviceActions;
|
||||||
|
}
|
||||||
|
|
||||||
private _computeEntityName(entity: EntityRegistryEntry) {
|
private _computeEntityName(entity: EntityRegistryEntry) {
|
||||||
if (entity.name) {
|
if (entity.name) {
|
||||||
return entity.name;
|
return entity.name;
|
||||||
@@ -895,26 +1021,13 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _renderIntegrationInfo(
|
private _renderIntegrationInfo(
|
||||||
device,
|
device: DeviceRegistryEntry,
|
||||||
integrations: ConfigEntry[],
|
integrations: ConfigEntry[],
|
||||||
deviceInfo: TemplateResult[],
|
deviceInfo: TemplateResult[],
|
||||||
deviceActions: (string | TemplateResult)[]
|
deviceAlerts: TemplateResult[]
|
||||||
): TemplateResult[] {
|
) {
|
||||||
const domains = integrations.map((int) => int.domain);
|
const domains = integrations.map((int) => int.domain);
|
||||||
const templates: TemplateResult[] = [];
|
|
||||||
if (domains.includes("mqtt")) {
|
|
||||||
import(
|
|
||||||
"./device-detail/integration-elements/mqtt/ha-device-actions-mqtt"
|
|
||||||
);
|
|
||||||
deviceActions.push(html`
|
|
||||||
<ha-device-actions-mqtt
|
|
||||||
.hass=${this.hass}
|
|
||||||
.device=${device}
|
|
||||||
></ha-device-actions-mqtt>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
if (domains.includes("zha")) {
|
if (domains.includes("zha")) {
|
||||||
import("./device-detail/integration-elements/zha/ha-device-actions-zha");
|
|
||||||
import("./device-detail/integration-elements/zha/ha-device-info-zha");
|
import("./device-detail/integration-elements/zha/ha-device-info-zha");
|
||||||
deviceInfo.push(html`
|
deviceInfo.push(html`
|
||||||
<ha-device-info-zha
|
<ha-device-info-zha
|
||||||
@@ -922,34 +1035,27 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
.device=${device}
|
.device=${device}
|
||||||
></ha-device-info-zha>
|
></ha-device-info-zha>
|
||||||
`);
|
`);
|
||||||
deviceActions.push(html`
|
|
||||||
<ha-device-actions-zha
|
|
||||||
.hass=${this.hass}
|
|
||||||
.device=${device}
|
|
||||||
></ha-device-actions-zha>
|
|
||||||
`);
|
|
||||||
}
|
}
|
||||||
if (domains.includes("zwave_js")) {
|
if (domains.includes("zwave_js")) {
|
||||||
import(
|
import(
|
||||||
"./device-detail/integration-elements/zwave_js/ha-device-info-zwave_js"
|
"./device-detail/integration-elements/zwave_js/ha-device-alerts-zwave_js"
|
||||||
);
|
);
|
||||||
import(
|
import(
|
||||||
"./device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js"
|
"./device-detail/integration-elements/zwave_js/ha-device-info-zwave_js"
|
||||||
);
|
);
|
||||||
|
deviceAlerts.push(html`
|
||||||
|
<ha-device-alerts-zwave_js
|
||||||
|
.hass=${this.hass}
|
||||||
|
.device=${device}
|
||||||
|
></ha-device-alerts-zwave_js>
|
||||||
|
`);
|
||||||
deviceInfo.push(html`
|
deviceInfo.push(html`
|
||||||
<ha-device-info-zwave_js
|
<ha-device-info-zwave_js
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.device=${device}
|
.device=${device}
|
||||||
></ha-device-info-zwave_js>
|
></ha-device-info-zwave_js>
|
||||||
`);
|
`);
|
||||||
deviceActions.push(html`
|
|
||||||
<ha-device-actions-zwave_js
|
|
||||||
.hass=${this.hass}
|
|
||||||
.device=${device}
|
|
||||||
></ha-device-actions-zwave_js>
|
|
||||||
`);
|
|
||||||
}
|
}
|
||||||
return templates;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _showSettings() {
|
private async _showSettings() {
|
||||||
@@ -1087,8 +1193,7 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _signUrl(ev) {
|
private async _signUrl(ev) {
|
||||||
const anchor = ev.target.closest("a");
|
const anchor = ev.currentTarget.closest("a");
|
||||||
ev.preventDefault();
|
|
||||||
const signedUrl = await getSignedPath(
|
const signedUrl = await getSignedPath(
|
||||||
this.hass,
|
this.hass,
|
||||||
anchor.getAttribute("href")
|
anchor.getAttribute("href")
|
||||||
@@ -1096,6 +1201,16 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
fileDownload(signedUrl.path);
|
fileDownload(signedUrl.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _deviceActionClicked(ev) {
|
||||||
|
if (!ev.currentTarget.action) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
(ev.currentTarget as any).action(ev);
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
@@ -1126,9 +1241,6 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.show-more {
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: var(--paper-font-headline_-_font-family);
|
font-family: var(--paper-font-headline_-_font-family);
|
||||||
@@ -1151,6 +1263,8 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
|
padding-inline-start: 8px;
|
||||||
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
.column,
|
.column,
|
||||||
@@ -1228,7 +1342,26 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
.items {
|
.items {
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ha-logbook {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
:host([narrow]) ha-logbook {
|
||||||
|
height: 235px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-config-device-page": HaConfigDevicePage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -541,7 +541,9 @@ export class HaConfigDeviceDashboard extends LitElement {
|
|||||||
.clear {
|
.clear {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
|
padding-inline-start: 8px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
haStyle,
|
haStyle,
|
||||||
|
@@ -107,10 +107,16 @@ export class EnergyBatterySettings extends LitElement {
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.battery.edit_battery_system"
|
||||||
|
)}
|
||||||
@click=${this._editSource}
|
@click=${this._editSource}
|
||||||
.path=${mdiPencil}
|
.path=${mdiPencil}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.battery.delete_battery_system"
|
||||||
|
)}
|
||||||
@click=${this._deleteSource}
|
@click=${this._deleteSource}
|
||||||
.path=${mdiDelete}
|
.path=${mdiDelete}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
|
@@ -94,10 +94,16 @@ export class EnergyGasSettings extends LitElement {
|
|||||||
: source.stat_energy_from}</span
|
: source.stat_energy_from}</span
|
||||||
>
|
>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.gas.edit_gas_source"
|
||||||
|
)}
|
||||||
@click=${this._editSource}
|
@click=${this._editSource}
|
||||||
.path=${mdiPencil}
|
.path=${mdiPencil}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.gas.delete_gas_source"
|
||||||
|
)}
|
||||||
@click=${this._deleteSource}
|
@click=${this._deleteSource}
|
||||||
.path=${mdiDelete}
|
.path=${mdiDelete}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
|
@@ -132,10 +132,16 @@ export class EnergyGridSettings extends LitElement {
|
|||||||
: flow.stat_energy_from}</span
|
: flow.stat_energy_from}</span
|
||||||
>
|
>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.grid.edit_consumption"
|
||||||
|
)}
|
||||||
@click=${this._editFromSource}
|
@click=${this._editFromSource}
|
||||||
.path=${mdiPencil}
|
.path=${mdiPencil}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.grid.delete_consumption"
|
||||||
|
)}
|
||||||
@click=${this._deleteFromSource}
|
@click=${this._deleteFromSource}
|
||||||
.path=${mdiDelete}
|
.path=${mdiDelete}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
@@ -171,10 +177,16 @@ export class EnergyGridSettings extends LitElement {
|
|||||||
: flow.stat_energy_to}</span
|
: flow.stat_energy_to}</span
|
||||||
>
|
>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.grid.edit_return"
|
||||||
|
)}
|
||||||
@click=${this._editToSource}
|
@click=${this._editToSource}
|
||||||
.path=${mdiPencil}
|
.path=${mdiPencil}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.grid.delete_return"
|
||||||
|
)}
|
||||||
@click=${this._deleteToSource}
|
@click=${this._deleteToSource}
|
||||||
.path=${mdiDelete}
|
.path=${mdiDelete}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
@@ -212,6 +224,9 @@ export class EnergyGridSettings extends LitElement {
|
|||||||
<ha-icon-button .path=${mdiPencil}></ha-icon-button>
|
<ha-icon-button .path=${mdiPencil}></ha-icon-button>
|
||||||
</a>
|
</a>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.grid.remove_co2_signal"
|
||||||
|
)}
|
||||||
@click=${this._removeCO2Sensor}
|
@click=${this._removeCO2Sensor}
|
||||||
.path=${mdiDelete}
|
.path=${mdiDelete}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
|
@@ -104,12 +104,18 @@ export class EnergySolarSettings extends LitElement {
|
|||||||
${this.info
|
${this.info
|
||||||
? html`
|
? html`
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.solar.edit_solar_production"
|
||||||
|
)}
|
||||||
@click=${this._editSource}
|
@click=${this._editSource}
|
||||||
.path=${mdiPencil}
|
.path=${mdiPencil}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.solar.delete_solar_production"
|
||||||
|
)}
|
||||||
@click=${this._deleteSource}
|
@click=${this._deleteSource}
|
||||||
.path=${mdiDelete}
|
.path=${mdiDelete}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
|
@@ -12,10 +12,12 @@ import {
|
|||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
import { domainIcon } from "../../../common/entity/domain_icon";
|
import { domainIcon } from "../../../common/entity/domain_icon";
|
||||||
|
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||||
import { stringCompare } from "../../../common/string/compare";
|
import { stringCompare } from "../../../common/string/compare";
|
||||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||||
import "../../../components/ha-alert";
|
import "../../../components/ha-alert";
|
||||||
@@ -24,8 +26,17 @@ import "../../../components/ha-expansion-panel";
|
|||||||
import "../../../components/ha-icon-picker";
|
import "../../../components/ha-icon-picker";
|
||||||
import "../../../components/ha-radio";
|
import "../../../components/ha-radio";
|
||||||
import "../../../components/ha-select";
|
import "../../../components/ha-select";
|
||||||
|
import "../../../components/ha-settings-row";
|
||||||
import "../../../components/ha-switch";
|
import "../../../components/ha-switch";
|
||||||
|
import type { HaSwitch } from "../../../components/ha-switch";
|
||||||
import "../../../components/ha-textfield";
|
import "../../../components/ha-textfield";
|
||||||
|
import {
|
||||||
|
CameraPreferences,
|
||||||
|
CAMERA_SUPPORT_STREAM,
|
||||||
|
fetchCameraPrefs,
|
||||||
|
STREAM_TYPE_HLS,
|
||||||
|
updateCameraPrefs,
|
||||||
|
} from "../../../data/camera";
|
||||||
import {
|
import {
|
||||||
ConfigEntry,
|
ConfigEntry,
|
||||||
deleteConfigEntry,
|
deleteConfigEntry,
|
||||||
@@ -133,6 +144,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _submitting?: boolean;
|
@state() private _submitting?: boolean;
|
||||||
|
|
||||||
|
@state() private _cameraPrefs?: CameraPreferences;
|
||||||
|
|
||||||
private _origEntityId!: string;
|
private _origEntityId!: string;
|
||||||
|
|
||||||
private _deviceLookup?: Record<string, DeviceRegistryEntry>;
|
private _deviceLookup?: Record<string, DeviceRegistryEntry>;
|
||||||
@@ -190,6 +203,20 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
const domain = computeDomain(this.entry.entity_id);
|
const domain = computeDomain(this.entry.entity_id);
|
||||||
|
|
||||||
|
if (domain === "camera" && isComponentLoaded(this.hass, "stream")) {
|
||||||
|
const stateObj: HassEntity | undefined =
|
||||||
|
this.hass.states[this.entry.entity_id];
|
||||||
|
if (
|
||||||
|
stateObj &&
|
||||||
|
supportsFeature(stateObj, CAMERA_SUPPORT_STREAM) &&
|
||||||
|
// The stream component for HLS streams supports a server-side pre-load
|
||||||
|
// option that client initiated WebRTC streams do not
|
||||||
|
stateObj.attributes.frontend_stream_type === STREAM_TYPE_HLS
|
||||||
|
) {
|
||||||
|
this._fetchCameraPrefs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (domain === "sensor") {
|
if (domain === "sensor") {
|
||||||
const stateObj: HassEntity | undefined =
|
const stateObj: HassEntity | undefined =
|
||||||
this.hass.states[this.entry.entity_id];
|
this.hass.states[this.entry.entity_id];
|
||||||
@@ -340,9 +367,24 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
|||||||
@selected=${this._switchAsChanged}
|
@selected=${this._switchAsChanged}
|
||||||
@closed=${stopPropagation}
|
@closed=${stopPropagation}
|
||||||
>
|
>
|
||||||
<mwc-list-item value="switch" selected>
|
<mwc-list-item
|
||||||
${domainToName(this.hass.localize, "switch")}</mwc-list-item
|
value="switch"
|
||||||
|
.selected=${!this._deviceClass ||
|
||||||
|
this._deviceClass === "switch"}
|
||||||
>
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.dialogs.entity_registry.editor.device_classes.switch.switch"
|
||||||
|
)}
|
||||||
|
</mwc-list-item>
|
||||||
|
<mwc-list-item
|
||||||
|
value="outlet"
|
||||||
|
.selected=${!this._deviceClass ||
|
||||||
|
this._deviceClass === "outlet"}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.dialogs.entity_registry.editor.device_classes.switch.outlet"
|
||||||
|
)}
|
||||||
|
</mwc-list-item>
|
||||||
<li divider role="separator"></li>
|
<li divider role="separator"></li>
|
||||||
${this._switchAsDomainsSorted(
|
${this._switchAsDomainsSorted(
|
||||||
SWITCH_AS_DOMAINS,
|
SWITCH_AS_DOMAINS,
|
||||||
@@ -392,7 +434,27 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
|||||||
@value-changed=${this._areaPicked}
|
@value-changed=${this._areaPicked}
|
||||||
></ha-area-picker>`
|
></ha-area-picker>`
|
||||||
: ""}
|
: ""}
|
||||||
|
${this._cameraPrefs
|
||||||
|
? html`
|
||||||
|
<ha-settings-row>
|
||||||
|
<span slot="heading"
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.dialogs.entity_registry.editor.preload_stream"
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
<span slot="description"
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.dialogs.entity_registry.editor.preload_stream_description"
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
<ha-switch
|
||||||
|
.checked=${this._cameraPrefs.preload_stream}
|
||||||
|
@change=${this._handleCameraPrefsChanged}
|
||||||
|
>
|
||||||
|
</ha-switch>
|
||||||
|
</ha-settings-row>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
<ha-expansion-panel
|
<ha-expansion-panel
|
||||||
.header=${this.hass.localize(
|
.header=${this.hass.localize(
|
||||||
"ui.dialogs.entity_registry.editor.advanced"
|
"ui.dialogs.entity_registry.editor.advanced"
|
||||||
@@ -570,7 +632,15 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
|||||||
if (ev.target.value === "") {
|
if (ev.target.value === "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._switchAs = ev.target.value;
|
|
||||||
|
// If value is "outlet" that means the user kept the "switch" domain, but actually changed
|
||||||
|
// the device_class of the switch to "outlet".
|
||||||
|
const switchAs = ev.target.value === "outlet" ? "switch" : ev.target.value;
|
||||||
|
this._switchAs = switchAs;
|
||||||
|
|
||||||
|
if (ev.target.value === "outlet" || ev.target.value === "switch") {
|
||||||
|
this._deviceClass = ev.target.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _areaPicked(ev: CustomEvent) {
|
private _areaPicked(ev: CustomEvent) {
|
||||||
@@ -578,6 +648,26 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
|||||||
this._areaId = ev.detail.value;
|
this._areaId = ev.detail.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _fetchCameraPrefs() {
|
||||||
|
this._cameraPrefs = await fetchCameraPrefs(this.hass, this.entry.entity_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _handleCameraPrefsChanged(ev) {
|
||||||
|
const checkbox = ev.currentTarget as HaSwitch;
|
||||||
|
try {
|
||||||
|
this._cameraPrefs = await updateCameraPrefs(
|
||||||
|
this.hass,
|
||||||
|
this.entry.entity_id,
|
||||||
|
{
|
||||||
|
preload_stream: checkbox.checked!,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err: any) {
|
||||||
|
showAlertDialog(this, { text: err.message });
|
||||||
|
checkbox.checked = !checkbox.checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _viewStatusChanged(ev: CustomEvent): void {
|
private _viewStatusChanged(ev: CustomEvent): void {
|
||||||
switch ((ev.target as any).value) {
|
switch ((ev.target as any).value) {
|
||||||
case "enabled":
|
case "enabled":
|
||||||
@@ -794,6 +884,12 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
|||||||
ha-switch {
|
ha-switch {
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
}
|
}
|
||||||
|
ha-settings-row {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
ha-settings-row ha-switch {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
ha-textfield {
|
ha-textfield {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
|
@@ -976,6 +976,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
.selected-txt {
|
.selected-txt {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
|
padding-inline-start: 16px;
|
||||||
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
.table-header .selected-txt {
|
.table-header .selected-txt {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
@@ -985,6 +987,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
.header-toolbar .header-btns {
|
.header-toolbar .header-btns {
|
||||||
margin-right: -12px;
|
margin-right: -12px;
|
||||||
|
margin-inline-end: -12px;
|
||||||
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
.header-btns {
|
.header-btns {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -999,7 +1003,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
.clear {
|
.clear {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
|
padding-inline-start: 8px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
@@ -1,14 +1,18 @@
|
|||||||
|
import "@material/mwc-list/mwc-list";
|
||||||
import "@material/mwc-list/mwc-list-item";
|
import "@material/mwc-list/mwc-list-item";
|
||||||
import { mdiDotsVertical } from "@mdi/js";
|
import { mdiDotsVertical } from "@mdi/js";
|
||||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
import "../../../components/buttons/ha-progress-button";
|
import "../../../components/buttons/ha-progress-button";
|
||||||
import "../../../components/ha-alert";
|
import "../../../components/ha-alert";
|
||||||
import "../../../components/ha-button-menu";
|
import "../../../components/ha-button-menu";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
|
import "../../../components/ha-clickable-list-item";
|
||||||
|
import "../../../components/ha-icon-next";
|
||||||
import "../../../components/ha-settings-row";
|
import "../../../components/ha-settings-row";
|
||||||
import { BOARD_NAMES } from "../../../data/hardware";
|
import { BOARD_NAMES, HardwareInfo } from "../../../data/hardware";
|
||||||
import {
|
import {
|
||||||
extractApiErrorMessage,
|
extractApiErrorMessage,
|
||||||
ignoreSupervisorError,
|
ignoreSupervisorError,
|
||||||
@@ -28,6 +32,8 @@ import {
|
|||||||
import "../../../layouts/hass-subpage";
|
import "../../../layouts/hass-subpage";
|
||||||
import { haStyle } from "../../../resources/styles";
|
import { haStyle } from "../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import { hardwareBrandsUrl } from "../../../util/brands-url";
|
||||||
|
import { showToast } from "../../../util/toast";
|
||||||
import { showhardwareAvailableDialog } from "./show-dialog-hardware-available";
|
import { showhardwareAvailableDialog } from "./show-dialog-hardware-available";
|
||||||
|
|
||||||
@customElement("ha-config-hardware")
|
@customElement("ha-config-hardware")
|
||||||
@@ -42,14 +48,36 @@ class HaConfigHardware extends LitElement {
|
|||||||
|
|
||||||
@state() private _hostData?: HassioHostInfo;
|
@state() private _hostData?: HassioHostInfo;
|
||||||
|
|
||||||
|
@state() private _hardwareInfo?: HardwareInfo;
|
||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues) {
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
if (isComponentLoaded(this.hass, "hassio")) {
|
this._load();
|
||||||
this._load();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
|
let boardId: string | undefined;
|
||||||
|
let boardName: string | undefined;
|
||||||
|
let imageURL: string | undefined;
|
||||||
|
let documentationURL: string | undefined;
|
||||||
|
|
||||||
|
if (this._hardwareInfo?.hardware.length) {
|
||||||
|
const boardData = this._hardwareInfo!.hardware[0];
|
||||||
|
|
||||||
|
boardId = boardData.board.hassio_board_id;
|
||||||
|
boardName = boardData.name;
|
||||||
|
documentationURL = boardData.url;
|
||||||
|
imageURL = hardwareBrandsUrl({
|
||||||
|
category: "boards",
|
||||||
|
manufacturer: boardData.board.manufacturer,
|
||||||
|
model: boardData.board.model,
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
});
|
||||||
|
} else if (this._OSData?.board) {
|
||||||
|
boardId = this._OSData.board;
|
||||||
|
boardName = BOARD_NAMES[this._OSData.board];
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<hass-subpage
|
<hass-subpage
|
||||||
back-path="/config/system"
|
back-path="/config/system"
|
||||||
@@ -68,6 +96,20 @@ class HaConfigHardware extends LitElement {
|
|||||||
"ui.panel.config.hardware.available_hardware.title"
|
"ui.panel.config.hardware.available_hardware.title"
|
||||||
)}</mwc-list-item
|
)}</mwc-list-item
|
||||||
>
|
>
|
||||||
|
${this._hostData
|
||||||
|
? html`
|
||||||
|
<mwc-list-item class="warning" @click=${this._hostReboot}
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.reboot_host"
|
||||||
|
)}</mwc-list-item
|
||||||
|
>
|
||||||
|
<mwc-list-item class="warning" @click=${this._hostShutdown}
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.shutdown_host"
|
||||||
|
)}</mwc-list-item
|
||||||
|
>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
</ha-button-menu>
|
</ha-button-menu>
|
||||||
${this._error
|
${this._error
|
||||||
? html`
|
? html`
|
||||||
@@ -76,57 +118,55 @@ class HaConfigHardware extends LitElement {
|
|||||||
>
|
>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
${this._OSData || this._hostData
|
${boardName
|
||||||
? html`
|
? html`
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<ha-card outlined>
|
<ha-card outlined>
|
||||||
${this._OSData?.board
|
<div class="card-content">
|
||||||
? html`
|
<mwc-list>
|
||||||
<div class="card-content">
|
<mwc-list-item
|
||||||
<ha-settings-row>
|
graphic=${ifDefined(imageURL ? "medium" : undefined)}
|
||||||
<span slot="heading"
|
.twoline=${Boolean(boardId)}
|
||||||
>${BOARD_NAMES[this._OSData.board] ||
|
>
|
||||||
this.hass.localize(
|
${imageURL
|
||||||
"ui.panel.config.hardware.board"
|
? html`<img slot="graphic" src=${imageURL} />`
|
||||||
)}</span
|
: ""}
|
||||||
|
<span class="primary-text">
|
||||||
|
${boardName ||
|
||||||
|
this.hass.localize("ui.panel.config.hardware.board")}
|
||||||
|
</span>
|
||||||
|
${boardId
|
||||||
|
? html`
|
||||||
|
<span class="secondary-text" slot="secondary"
|
||||||
|
>${boardId}</span
|
||||||
|
>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</mwc-list-item>
|
||||||
|
${documentationURL
|
||||||
|
? html`
|
||||||
|
<ha-clickable-list-item
|
||||||
|
.href=${documentationURL}
|
||||||
|
openNewTab
|
||||||
|
twoline
|
||||||
|
hasMeta
|
||||||
>
|
>
|
||||||
<div slot="description">
|
<span
|
||||||
<span class="value">${this._OSData.board}</span>
|
>${this.hass.localize(
|
||||||
</div>
|
"ui.panel.config.hardware.documentation"
|
||||||
</ha-settings-row>
|
)}</span
|
||||||
</div>
|
>
|
||||||
`
|
<span slot="secondary"
|
||||||
: ""}
|
>${this.hass.localize(
|
||||||
${this._hostData
|
"ui.panel.config.hardware.documentation_description"
|
||||||
? html`
|
)}</span
|
||||||
<div class="card-actions">
|
>
|
||||||
${this._hostData.features.includes("reboot")
|
<ha-icon-next slot="meta"></ha-icon-next>
|
||||||
? html`
|
</ha-clickable-list-item>
|
||||||
<ha-progress-button
|
`
|
||||||
class="warning"
|
: ""}
|
||||||
@click=${this._hostReboot}
|
</mwc-list>
|
||||||
>
|
</div>
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.hardware.reboot_host"
|
|
||||||
)}
|
|
||||||
</ha-progress-button>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
${this._hostData.features.includes("shutdown")
|
|
||||||
? html`
|
|
||||||
<ha-progress-button
|
|
||||||
class="warning"
|
|
||||||
@click=${this._hostShutdown}
|
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.hardware.shutdown_host"
|
|
||||||
)}
|
|
||||||
</ha-progress-button>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</ha-card>
|
</ha-card>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
@@ -136,9 +176,17 @@ class HaConfigHardware extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _load() {
|
private async _load() {
|
||||||
|
const isHassioLoaded = isComponentLoaded(this.hass, "hassio");
|
||||||
try {
|
try {
|
||||||
this._OSData = await fetchHassioHassOsInfo(this.hass);
|
if (isComponentLoaded(this.hass, "hardware")) {
|
||||||
this._hostData = await fetchHassioHostInfo(this.hass);
|
this._hardwareInfo = await this.hass.callWS({ type: "hardware/info" });
|
||||||
|
} else if (isHassioLoaded) {
|
||||||
|
this._OSData = await fetchHassioHassOsInfo(this.hass);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHassioLoaded) {
|
||||||
|
this._hostData = await fetchHassioHostInfo(this.hass);
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this._error = err.message || err;
|
this._error = err.message || err;
|
||||||
}
|
}
|
||||||
@@ -148,10 +196,7 @@ class HaConfigHardware extends LitElement {
|
|||||||
showhardwareAvailableDialog(this);
|
showhardwareAvailableDialog(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _hostReboot(ev: CustomEvent): Promise<void> {
|
private async _hostReboot(): Promise<void> {
|
||||||
const button = ev.currentTarget as any;
|
|
||||||
button.progress = true;
|
|
||||||
|
|
||||||
const confirmed = await showConfirmationDialog(this, {
|
const confirmed = await showConfirmationDialog(this, {
|
||||||
title: this.hass.localize("ui.panel.config.hardware.reboot_host"),
|
title: this.hass.localize("ui.panel.config.hardware.reboot_host"),
|
||||||
text: this.hass.localize("ui.panel.config.hardware.reboot_host_confirm"),
|
text: this.hass.localize("ui.panel.config.hardware.reboot_host_confirm"),
|
||||||
@@ -160,10 +205,14 @@ class HaConfigHardware extends LitElement {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
button.progress = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showToast(this, {
|
||||||
|
message: this.hass.localize("ui.panel.config.hardware.rebooting_host"),
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await rebootHost(this.hass);
|
await rebootHost(this.hass);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -177,13 +226,9 @@ class HaConfigHardware extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
button.progress = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _hostShutdown(ev: CustomEvent): Promise<void> {
|
private async _hostShutdown(): Promise<void> {
|
||||||
const button = ev.currentTarget as any;
|
|
||||||
button.progress = true;
|
|
||||||
|
|
||||||
const confirmed = await showConfirmationDialog(this, {
|
const confirmed = await showConfirmationDialog(this, {
|
||||||
title: this.hass.localize("ui.panel.config.hardware.shutdown_host"),
|
title: this.hass.localize("ui.panel.config.hardware.shutdown_host"),
|
||||||
text: this.hass.localize(
|
text: this.hass.localize(
|
||||||
@@ -194,10 +239,16 @@ class HaConfigHardware extends LitElement {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
button.progress = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showToast(this, {
|
||||||
|
message: this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.host_shutting_down"
|
||||||
|
),
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await shutdownHost(this.hass);
|
await shutdownHost(this.hass);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -211,7 +262,6 @@ class HaConfigHardware extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
button.progress = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = [
|
static styles = [
|
||||||
@@ -234,17 +284,18 @@ class HaConfigHardware extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 16px 16px 0 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
ha-button-menu {
|
ha-button-menu {
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
--mdc-menu-min-width: 200px;
|
--mdc-menu-min-width: 200px;
|
||||||
}
|
}
|
||||||
.card-actions {
|
|
||||||
height: 48px;
|
.primary-text {
|
||||||
display: flex;
|
font-size: 16px;
|
||||||
justify-content: space-between;
|
}
|
||||||
align-items: center;
|
.secondary-text {
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
@@ -4,9 +4,7 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
|||||||
import "../../../components/ha-logo-svg";
|
import "../../../components/ha-logo-svg";
|
||||||
import {
|
import {
|
||||||
fetchHassioHassOsInfo,
|
fetchHassioHassOsInfo,
|
||||||
fetchHassioHostInfo,
|
|
||||||
HassioHassOSInfo,
|
HassioHassOSInfo,
|
||||||
HassioHostInfo,
|
|
||||||
} from "../../../data/hassio/host";
|
} from "../../../data/hassio/host";
|
||||||
import { fetchHassioInfo, HassioInfo } from "../../../data/hassio/supervisor";
|
import { fetchHassioInfo, HassioInfo } from "../../../data/hassio/supervisor";
|
||||||
import "../../../layouts/hass-subpage";
|
import "../../../layouts/hass-subpage";
|
||||||
@@ -28,8 +26,6 @@ class HaConfigInfo extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public route!: Route;
|
@property({ attribute: false }) public route!: Route;
|
||||||
|
|
||||||
@state() private _hostInfo?: HassioHostInfo;
|
|
||||||
|
|
||||||
@state() private _osInfo?: HassioHassOSInfo;
|
@state() private _osInfo?: HassioHassOSInfo;
|
||||||
|
|
||||||
@state() private _hassioInfo?: HassioInfo;
|
@state() private _hassioInfo?: HassioInfo;
|
||||||
@@ -71,12 +67,6 @@ class HaConfigInfo extends LitElement {
|
|||||||
${this._osInfo?.version
|
${this._osInfo?.version
|
||||||
? html`<h3>Home Assistant OS ${this._osInfo.version}</h3>`
|
? html`<h3>Home Assistant OS ${this._osInfo.version}</h3>`
|
||||||
: ""}
|
: ""}
|
||||||
${this._hostInfo
|
|
||||||
? html`
|
|
||||||
<h4>Kernel version ${this._hostInfo.kernel}</h4>
|
|
||||||
<h4>Agent version ${this._hostInfo.agent_version}</h4>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
<p>
|
<p>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.info.path_configuration",
|
"ui.panel.config.info.path_configuration",
|
||||||
@@ -177,15 +167,13 @@ class HaConfigInfo extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _loadSupervisorInfo(): Promise<void> {
|
private async _loadSupervisorInfo(): Promise<void> {
|
||||||
const [hostInfo, osInfo, hassioInfo] = await Promise.all([
|
const [osInfo, hassioInfo] = await Promise.all([
|
||||||
fetchHassioHostInfo(this.hass),
|
|
||||||
fetchHassioHassOsInfo(this.hass),
|
fetchHassioHassOsInfo(this.hass),
|
||||||
fetchHassioInfo(this.hass),
|
fetchHassioInfo(this.hass),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this._hassioInfo = hassioInfo;
|
this._hassioInfo = hassioInfo;
|
||||||
this._osInfo = osInfo;
|
this._osInfo = osInfo;
|
||||||
this._hostInfo = hostInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
@@ -46,7 +46,6 @@ import {
|
|||||||
} from "../../../data/entity_registry";
|
} from "../../../data/entity_registry";
|
||||||
import {
|
import {
|
||||||
domainToName,
|
domainToName,
|
||||||
fetchIntegrationManifest,
|
|
||||||
fetchIntegrationManifests,
|
fetchIntegrationManifests,
|
||||||
IntegrationManifest,
|
IntegrationManifest,
|
||||||
} from "../../../data/integration";
|
} from "../../../data/integration";
|
||||||
@@ -156,17 +155,20 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
|||||||
this._deviceRegistryEntries = entries;
|
this._deviceRegistryEntries = entries;
|
||||||
}),
|
}),
|
||||||
subscribeConfigFlowInProgress(this.hass, async (flowsInProgress) => {
|
subscribeConfigFlowInProgress(this.hass, async (flowsInProgress) => {
|
||||||
const translationsPromisses: Promise<LocalizeFunc>[] = [];
|
const integrations: Set<string> = new Set();
|
||||||
|
const manifests: Set<string> = new Set();
|
||||||
flowsInProgress.forEach((flow) => {
|
flowsInProgress.forEach((flow) => {
|
||||||
// To render title placeholders
|
// To render title placeholders
|
||||||
if (flow.context.title_placeholders) {
|
if (flow.context.title_placeholders) {
|
||||||
translationsPromisses.push(
|
integrations.add(flow.handler);
|
||||||
this.hass.loadBackendTranslation("config", flow.handler)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
this._fetchManifest(flow.handler);
|
manifests.add(flow.handler);
|
||||||
});
|
});
|
||||||
await Promise.all(translationsPromisses);
|
await this.hass.loadBackendTranslation(
|
||||||
|
"config",
|
||||||
|
Array.from(integrations)
|
||||||
|
);
|
||||||
|
this._fetchIntegrationManifests(manifests);
|
||||||
await nextRender();
|
await nextRender();
|
||||||
this._configEntriesInProgress = flowsInProgress.map((flow) => ({
|
this._configEntriesInProgress = flowsInProgress.map((flow) => ({
|
||||||
...flow,
|
...flow,
|
||||||
@@ -565,8 +567,8 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
|||||||
await scanUSBDevices(this.hass);
|
await scanUSBDevices(this.hass);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _fetchManifests() {
|
private async _fetchManifests(integrations?: string[]) {
|
||||||
const fetched = await fetchIntegrationManifests(this.hass);
|
const fetched = await fetchIntegrationManifests(this.hass, integrations);
|
||||||
// Make a copy so we can keep track of previously loaded manifests
|
// Make a copy so we can keep track of previously loaded manifests
|
||||||
// for discovered flows (which are not part of these results)
|
// for discovered flows (which are not part of these results)
|
||||||
const manifests = { ...this._manifests };
|
const manifests = { ...this._manifests };
|
||||||
@@ -574,23 +576,25 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
|||||||
this._manifests = manifests;
|
this._manifests = manifests;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _fetchManifest(domain: string) {
|
private async _fetchIntegrationManifests(integrations: Set<string>) {
|
||||||
if (domain in this._manifests) {
|
const manifestsToFetch: string[] = [];
|
||||||
return;
|
for (const integration of integrations) {
|
||||||
}
|
if (integration in this._manifests) {
|
||||||
if (this._extraFetchedManifests) {
|
continue;
|
||||||
if (this._extraFetchedManifests.has(domain)) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} else {
|
if (this._extraFetchedManifests) {
|
||||||
this._extraFetchedManifests = new Set();
|
if (this._extraFetchedManifests.has(integration)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._extraFetchedManifests = new Set();
|
||||||
|
}
|
||||||
|
this._extraFetchedManifests.add(integration);
|
||||||
|
manifestsToFetch.push(integration);
|
||||||
|
}
|
||||||
|
if (manifestsToFetch.length) {
|
||||||
|
await this._fetchManifests(manifestsToFetch);
|
||||||
}
|
}
|
||||||
this._extraFetchedManifests.add(domain);
|
|
||||||
const manifest = await fetchIntegrationManifest(this.hass, domain);
|
|
||||||
this._manifests = {
|
|
||||||
...this._manifests,
|
|
||||||
[domain]: manifest,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleEntryRemoved(ev: HASSDomEvent<ConfigEntryRemovedEvent>) {
|
private _handleEntryRemoved(ev: HASSDomEvent<ConfigEntryRemovedEvent>) {
|
||||||
|
@@ -202,10 +202,9 @@ class DialogZWaveJSHealNetwork extends LitElement {
|
|||||||
if (!this.hass) {
|
if (!this.hass) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const network: ZWaveJSNetwork = await fetchZwaveNetworkStatus(
|
const network: ZWaveJSNetwork = await fetchZwaveNetworkStatus(this.hass!, {
|
||||||
this.hass!,
|
entry_id: this.entry_id!,
|
||||||
this.entry_id!
|
});
|
||||||
);
|
|
||||||
if (network.controller.is_heal_network_active) {
|
if (network.controller.is_heal_network_active) {
|
||||||
this._status = "started";
|
this._status = "started";
|
||||||
this._subscribed = subscribeHealZwaveNetworkProgress(
|
this._subscribed = subscribeHealZwaveNetworkProgress(
|
||||||
|
@@ -22,10 +22,6 @@ import { ZWaveJSHealNodeDialogParams } from "./show-dialog-zwave_js-heal-node";
|
|||||||
class DialogZWaveJSHealNode extends LitElement {
|
class DialogZWaveJSHealNode extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@state() private entry_id?: string;
|
|
||||||
|
|
||||||
@state() private node_id?: number;
|
|
||||||
|
|
||||||
@state() private device?: DeviceRegistryEntry;
|
@state() private device?: DeviceRegistryEntry;
|
||||||
|
|
||||||
@state() private _status?: string;
|
@state() private _status?: string;
|
||||||
@@ -33,16 +29,12 @@ class DialogZWaveJSHealNode extends LitElement {
|
|||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
public showDialog(params: ZWaveJSHealNodeDialogParams): void {
|
public showDialog(params: ZWaveJSHealNodeDialogParams): void {
|
||||||
this.entry_id = params.entry_id;
|
|
||||||
this.device = params.device;
|
this.device = params.device;
|
||||||
this.node_id = params.node_id;
|
|
||||||
this._fetchData();
|
this._fetchData();
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): void {
|
public closeDialog(): void {
|
||||||
this.entry_id = undefined;
|
|
||||||
this._status = undefined;
|
this._status = undefined;
|
||||||
this.node_id = undefined;
|
|
||||||
this.device = undefined;
|
this.device = undefined;
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
|
|
||||||
@@ -50,7 +42,7 @@ class DialogZWaveJSHealNode extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this.entry_id || !this.device) {
|
if (!this.device) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,10 +198,9 @@ class DialogZWaveJSHealNode extends LitElement {
|
|||||||
if (!this.hass) {
|
if (!this.hass) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const network: ZWaveJSNetwork = await fetchZwaveNetworkStatus(
|
const network: ZWaveJSNetwork = await fetchZwaveNetworkStatus(this.hass!, {
|
||||||
this.hass!,
|
device_id: this.device!.id,
|
||||||
this.entry_id!
|
});
|
||||||
);
|
|
||||||
if (network.controller.is_heal_network_active) {
|
if (network.controller.is_heal_network_active) {
|
||||||
this._status = "network-healing";
|
this._status = "network-healing";
|
||||||
}
|
}
|
||||||
@@ -221,11 +212,7 @@ class DialogZWaveJSHealNode extends LitElement {
|
|||||||
}
|
}
|
||||||
this._status = "started";
|
this._status = "started";
|
||||||
try {
|
try {
|
||||||
this._status = (await healZwaveNode(
|
this._status = (await healZwaveNode(this.hass, this.device!.id))
|
||||||
this.hass,
|
|
||||||
this.entry_id!,
|
|
||||||
this.node_id!
|
|
||||||
))
|
|
||||||
? "finished"
|
? "finished"
|
||||||
: "failed";
|
: "failed";
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
@@ -15,9 +15,7 @@ import { ZWaveJSReinterviewNodeDialogParams } from "./show-dialog-zwave_js-reint
|
|||||||
class DialogZWaveJSReinterviewNode extends LitElement {
|
class DialogZWaveJSReinterviewNode extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@state() private entry_id?: string;
|
@state() private device_id?: string;
|
||||||
|
|
||||||
@state() private node_id?: number;
|
|
||||||
|
|
||||||
@state() private _status?: string;
|
@state() private _status?: string;
|
||||||
|
|
||||||
@@ -29,12 +27,11 @@ class DialogZWaveJSReinterviewNode extends LitElement {
|
|||||||
params: ZWaveJSReinterviewNodeDialogParams
|
params: ZWaveJSReinterviewNodeDialogParams
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this._stages = undefined;
|
this._stages = undefined;
|
||||||
this.entry_id = params.entry_id;
|
this.device_id = params.device_id;
|
||||||
this.node_id = params.node_id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this.entry_id) {
|
if (!this.device_id) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,8 +156,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
|
|||||||
}
|
}
|
||||||
this._subscribed = reinterviewZwaveNode(
|
this._subscribed = reinterviewZwaveNode(
|
||||||
this.hass,
|
this.hass,
|
||||||
this.entry_id!,
|
this.device_id!,
|
||||||
this.node_id!,
|
|
||||||
this._handleMessage.bind(this)
|
this._handleMessage.bind(this)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -194,8 +190,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): void {
|
public closeDialog(): void {
|
||||||
this.entry_id = undefined;
|
this.device_id = undefined;
|
||||||
this.node_id = undefined;
|
|
||||||
this._status = undefined;
|
this._status = undefined;
|
||||||
this._stages = undefined;
|
this._stages = undefined;
|
||||||
|
|
||||||
|
@@ -18,9 +18,7 @@ import { ZWaveJSRemoveFailedNodeDialogParams } from "./show-dialog-zwave_js-remo
|
|||||||
class DialogZWaveJSRemoveFailedNode extends LitElement {
|
class DialogZWaveJSRemoveFailedNode extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@state() private entry_id?: string;
|
@state() private device_id?: string;
|
||||||
|
|
||||||
@state() private node_id?: number;
|
|
||||||
|
|
||||||
@state() private _status = "";
|
@state() private _status = "";
|
||||||
|
|
||||||
@@ -38,13 +36,12 @@ class DialogZWaveJSRemoveFailedNode extends LitElement {
|
|||||||
public async showDialog(
|
public async showDialog(
|
||||||
params: ZWaveJSRemoveFailedNodeDialogParams
|
params: ZWaveJSRemoveFailedNodeDialogParams
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.entry_id = params.entry_id;
|
this.device_id = params.device_id;
|
||||||
this.node_id = params.node_id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): void {
|
public closeDialog(): void {
|
||||||
this._unsubscribe();
|
this._unsubscribe();
|
||||||
this.entry_id = undefined;
|
this.device_id = undefined;
|
||||||
this._status = "";
|
this._status = "";
|
||||||
|
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
@@ -56,7 +53,7 @@ class DialogZWaveJSRemoveFailedNode extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this.entry_id || !this.node_id) {
|
if (!this.device_id) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,8 +163,7 @@ class DialogZWaveJSRemoveFailedNode extends LitElement {
|
|||||||
this._status = "started";
|
this._status = "started";
|
||||||
this._subscribed = removeFailedZwaveNode(
|
this._subscribed = removeFailedZwaveNode(
|
||||||
this.hass,
|
this.hass,
|
||||||
this.entry_id!,
|
this.device_id!,
|
||||||
this.node_id!,
|
|
||||||
(message: any) => this._handleMessage(message)
|
(message: any) => this._handleMessage(message)
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
this._status = "failed";
|
this._status = "failed";
|
||||||
|
@@ -2,8 +2,6 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
|
|||||||
import { DeviceRegistryEntry } from "../../../../../data/device_registry";
|
import { DeviceRegistryEntry } from "../../../../../data/device_registry";
|
||||||
|
|
||||||
export interface ZWaveJSHealNodeDialogParams {
|
export interface ZWaveJSHealNodeDialogParams {
|
||||||
entry_id: string;
|
|
||||||
node_id: number;
|
|
||||||
device: DeviceRegistryEntry;
|
device: DeviceRegistryEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||||
|
|
||||||
export interface ZWaveJSReinterviewNodeDialogParams {
|
export interface ZWaveJSReinterviewNodeDialogParams {
|
||||||
entry_id: string;
|
device_id: string;
|
||||||
node_id: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadReinterviewNodeDialog = () =>
|
export const loadReinterviewNodeDialog = () =>
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||||
|
|
||||||
export interface ZWaveJSRemoveFailedNodeDialogParams {
|
export interface ZWaveJSRemoveFailedNodeDialogParams {
|
||||||
entry_id: string;
|
device_id: string;
|
||||||
node_id: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadRemoveFailedNodeDialog = () =>
|
export const loadRemoveFailedNodeDialog = () =>
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import "@material/mwc-button/mwc-button";
|
import "@material/mwc-list/mwc-list";
|
||||||
|
import "@material/mwc-list/mwc-list-item";
|
||||||
import {
|
import {
|
||||||
mdiAlertCircle,
|
mdiAlertCircle,
|
||||||
mdiCheckCircle,
|
mdiCheckCircle,
|
||||||
@@ -11,21 +12,24 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import "../../../../../components/ha-card";
|
import "../../../../../components/ha-card";
|
||||||
import "../../../../../components/ha-icon-button";
|
import "../../../../../components/ha-icon-button";
|
||||||
|
import "../../../../../components/ha-expansion-panel";
|
||||||
import "../../../../../components/ha-fab";
|
import "../../../../../components/ha-fab";
|
||||||
|
import "../../../../../components/ha-help-tooltip";
|
||||||
import "../../../../../components/ha-icon-next";
|
import "../../../../../components/ha-icon-next";
|
||||||
import "../../../../../components/ha-svg-icon";
|
import "../../../../../components/ha-svg-icon";
|
||||||
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import {
|
import {
|
||||||
fetchZwaveDataCollectionStatus,
|
fetchZwaveDataCollectionStatus,
|
||||||
fetchZwaveNetworkStatus,
|
fetchZwaveNetworkStatus,
|
||||||
fetchZwaveNodeStatus,
|
|
||||||
fetchZwaveProvisioningEntries,
|
fetchZwaveProvisioningEntries,
|
||||||
InclusionState,
|
InclusionState,
|
||||||
setZwaveDataCollectionPreference,
|
setZwaveDataCollectionPreference,
|
||||||
stopZwaveExclusion,
|
stopZwaveExclusion,
|
||||||
stopZwaveInclusion,
|
stopZwaveInclusion,
|
||||||
|
subscribeZwaveControllerStatistics,
|
||||||
ZWaveJSClient,
|
ZWaveJSClient,
|
||||||
|
ZWaveJSControllerStatisticsUpdatedMessage,
|
||||||
ZWaveJSNetwork,
|
ZWaveJSNetwork,
|
||||||
ZWaveJSNodeStatus,
|
|
||||||
ZwaveJSProvisioningEntry,
|
ZwaveJSProvisioningEntry,
|
||||||
} from "../../../../../data/zwave_js";
|
} from "../../../../../data/zwave_js";
|
||||||
import {
|
import {
|
||||||
@@ -43,9 +47,10 @@ import { showZWaveJSRemoveNodeDialog } from "./show-dialog-zwave_js-remove-node"
|
|||||||
import { configTabs } from "./zwave_js-config-router";
|
import { configTabs } from "./zwave_js-config-router";
|
||||||
import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow";
|
import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow";
|
||||||
import { computeRTL } from "../../../../../common/util/compute_rtl";
|
import { computeRTL } from "../../../../../common/util/compute_rtl";
|
||||||
|
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
|
||||||
|
|
||||||
@customElement("zwave_js-config-dashboard")
|
@customElement("zwave_js-config-dashboard")
|
||||||
class ZWaveJSConfigDashboard extends LitElement {
|
class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
||||||
@property({ type: Object }) public hass!: HomeAssistant;
|
@property({ type: Object }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ type: Object }) public route!: Route;
|
@property({ type: Object }) public route!: Route;
|
||||||
@@ -54,14 +59,12 @@ class ZWaveJSConfigDashboard extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public isWide!: boolean;
|
@property({ type: Boolean }) public isWide!: boolean;
|
||||||
|
|
||||||
@property() public configEntryId?: string;
|
@property() public configEntryId!: string;
|
||||||
|
|
||||||
@state() private _configEntry?: ConfigEntry;
|
@state() private _configEntry?: ConfigEntry;
|
||||||
|
|
||||||
@state() private _network?: ZWaveJSNetwork;
|
@state() private _network?: ZWaveJSNetwork;
|
||||||
|
|
||||||
@state() private _nodes?: ZWaveJSNodeStatus[];
|
|
||||||
|
|
||||||
@state() private _provisioningEntries?: ZwaveJSProvisioningEntry[];
|
@state() private _provisioningEntries?: ZwaveJSProvisioningEntry[];
|
||||||
|
|
||||||
@state() private _status?: ZWaveJSClient["state"];
|
@state() private _status?: ZWaveJSClient["state"];
|
||||||
@@ -70,12 +73,30 @@ class ZWaveJSConfigDashboard extends LitElement {
|
|||||||
|
|
||||||
@state() private _dataCollectionOptIn?: boolean;
|
@state() private _dataCollectionOptIn?: boolean;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _statistics?: ZWaveJSControllerStatisticsUpdatedMessage;
|
||||||
|
|
||||||
protected firstUpdated() {
|
protected firstUpdated() {
|
||||||
if (this.hass) {
|
if (this.hass) {
|
||||||
this._fetchData();
|
this._fetchData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public hassSubscribe(): Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> {
|
||||||
|
return [
|
||||||
|
subscribeZwaveControllerStatistics(
|
||||||
|
this.hass,
|
||||||
|
this.configEntryId,
|
||||||
|
(message) => {
|
||||||
|
if (!this.hasUpdated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._statistics = message;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this._configEntry) {
|
if (!this._configEntry) {
|
||||||
return html``;
|
return html``;
|
||||||
@@ -84,9 +105,8 @@ class ZWaveJSConfigDashboard extends LitElement {
|
|||||||
if (ERROR_STATES.includes(this._configEntry.state)) {
|
if (ERROR_STATES.includes(this._configEntry.state)) {
|
||||||
return this._renderErrorScreen();
|
return this._renderErrorScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
const notReadyDevices =
|
const notReadyDevices =
|
||||||
this._nodes?.filter((node) => !node.ready).length ?? 0;
|
this._network?.controller.nodes.filter((node) => !node.ready).length ?? 0;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<hass-tabs-subpage
|
<hass-tabs-subpage
|
||||||
@@ -216,22 +236,178 @@ class ZWaveJSConfigDashboard extends LitElement {
|
|||||||
</ha-card>
|
</ha-card>
|
||||||
<ha-card header="Diagnostics">
|
<ha-card header="Diagnostics">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
${this.hass.localize(
|
<div class="row">
|
||||||
"ui.panel.config.zwave_js.dashboard.driver_version"
|
<span>
|
||||||
)}:
|
${this.hass.localize(
|
||||||
${this._network.client.driver_version}<br />
|
"ui.panel.config.zwave_js.dashboard.driver_version"
|
||||||
${this.hass.localize(
|
)}:
|
||||||
"ui.panel.config.zwave_js.dashboard.server_version"
|
</span>
|
||||||
)}:
|
<span>${this._network.client.driver_version}</span>
|
||||||
${this._network.client.server_version}<br />
|
</div>
|
||||||
${this.hass.localize(
|
<div class="row">
|
||||||
"ui.panel.config.zwave_js.dashboard.home_id"
|
<span>
|
||||||
)}:
|
${this.hass.localize(
|
||||||
${this._network.controller.home_id}<br />
|
"ui.panel.config.zwave_js.dashboard.server_version"
|
||||||
${this.hass.localize(
|
)}:
|
||||||
"ui.panel.config.zwave_js.dashboard.server_url"
|
</span>
|
||||||
)}:
|
<span>${this._network.client.server_version}</span>
|
||||||
${this._network.client.ws_server_url}<br />
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.dashboard.home_id"
|
||||||
|
)}:
|
||||||
|
</span>
|
||||||
|
<span>${this._network.controller.home_id}</span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.dashboard.server_url"
|
||||||
|
)}:
|
||||||
|
</span>
|
||||||
|
<span>${this._network.client.ws_server_url}</span>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<ha-expansion-panel
|
||||||
|
.header=${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.dashboard.statistics.title"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<mwc-list noninteractive>
|
||||||
|
<mwc-list-item twoline hasmeta>
|
||||||
|
<span>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.dashboard.statistics.messages_tx.label"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="secondary">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.dashboard.statistics.messages_tx.tooltip"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="meta"
|
||||||
|
>${this._statistics?.messages_tx ?? 0}</span
|
||||||
|
>
|
||||||
|
</mwc-list-item>
|
||||||
|
<mwc-list-item twoline hasmeta>
|
||||||
|
<span>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.dashboard.statistics.messages_rx.label"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="secondary">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.dashboard.statistics.messages_rx.tooltip"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="meta"
|
||||||
|
>${this._statistics?.messages_rx ?? 0}</span
|
||||||
|
>
|
||||||
|
</mwc-list-item>
|
||||||
|
<mwc-list-item twoline hasmeta>
|
||||||
|
<span>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.dashboard.statistics.messages_dropped_tx.label"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="secondary">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.dashboard.statistics.messages_dropped_tx.tooltip"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="meta"
|
||||||
|
>${this._statistics?.messages_dropped_tx ?? 0}</span
|
||||||
|
>
|
||||||
|
</mwc-list-item>
|
||||||
|
<mwc-list-item twoline hasmeta>
|
||||||
|
<span>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.dashboard.statistics.messages_dropped_rx.label"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="secondary">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.dashboard.statistics.messages_dropped_rx.tooltip"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="meta"
|
||||||
|
>${this._statistics?.messages_dropped_rx ?? 0}</span
|
||||||
|
>
|
||||||
|
</mwc-list-item>
|
||||||
|
<mwc-list-item twoline hasmeta>
|
||||||
|
<span>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.dashboard.statistics.nak.label"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="secondary">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.dashboard.statistics.nak.tooltip"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="meta">${this._statistics?.nak ?? 0}</span>
|
||||||
|
</mwc-list-item>
|
||||||
|
<mwc-list-item twoline hasmeta>
|
||||||
|
<span>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.dashboard.statistics.can.label"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="secondary">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.dashboard.statistics.can.tooltip"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="meta">${this._statistics?.can ?? 0}</span>
|
||||||
|
</mwc-list-item>
|
||||||
|
<mwc-list-item twoline hasmeta>
|
||||||
|
<span>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.dashboard.statistics.timeout_ack.label"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="secondary">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.dashboard.statistics.timeout_ack.tooltip"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="meta"
|
||||||
|
>${this._statistics?.timeout_ack ?? 0}</span
|
||||||
|
>
|
||||||
|
</mwc-list-item>
|
||||||
|
<mwc-list-item twoline hasmeta>
|
||||||
|
<span>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.dashboard.statistics.timeout_response.label"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="secondary">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.dashboard.statistics.timeout_response.tooltip"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="meta"
|
||||||
|
>${this._statistics?.timeout_response ?? 0}</span
|
||||||
|
>
|
||||||
|
</mwc-list-item>
|
||||||
|
<mwc-list-item twoline hasmeta>
|
||||||
|
<span>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.dashboard.statistics.timeout_callback.label"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="secondary">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.dashboard.statistics.timeout_callback.tooltip"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="meta"
|
||||||
|
>${this._statistics?.timeout_callback ?? 0}</span
|
||||||
|
>
|
||||||
|
</mwc-list-item>
|
||||||
|
</mwc-list>
|
||||||
|
</ha-expansion-panel>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<mwc-button
|
<mwc-button
|
||||||
@@ -288,7 +464,7 @@ class ZWaveJSConfigDashboard extends LitElement {
|
|||||||
data collected, can be found in the
|
data collected, can be found in the
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://zwave-js.github.io/node-zwave-js/#/data-collection/data-collection?id=usage-statistics"
|
href="https://zwave-js.github.io/node-zwave-js/#/data-collection/data-collection"
|
||||||
>Z-Wave JS data collection documentation</a
|
>Z-Wave JS data collection documentation</a
|
||||||
>.
|
>.
|
||||||
</p>
|
</p>
|
||||||
@@ -388,7 +564,7 @@ class ZWaveJSConfigDashboard extends LitElement {
|
|||||||
domain: "zwave_js",
|
domain: "zwave_js",
|
||||||
});
|
});
|
||||||
this._configEntry = configEntries.find(
|
this._configEntry = configEntries.find(
|
||||||
(entry) => entry.entry_id === this.configEntryId!
|
(entry) => entry.entry_id === this.configEntryId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (ERROR_STATES.includes(this._configEntry!.state)) {
|
if (ERROR_STATES.includes(this._configEntry!.state)) {
|
||||||
@@ -397,7 +573,7 @@ class ZWaveJSConfigDashboard extends LitElement {
|
|||||||
|
|
||||||
const [network, dataCollectionStatus, provisioningEntries] =
|
const [network, dataCollectionStatus, provisioningEntries] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
fetchZwaveNetworkStatus(this.hass!, this.configEntryId),
|
fetchZwaveNetworkStatus(this.hass!, { entry_id: this.configEntryId }),
|
||||||
fetchZwaveDataCollectionStatus(this.hass!, this.configEntryId),
|
fetchZwaveDataCollectionStatus(this.hass!, this.configEntryId),
|
||||||
fetchZwaveProvisioningEntries(this.hass!, this.configEntryId),
|
fetchZwaveProvisioningEntries(this.hass!, this.configEntryId),
|
||||||
]);
|
]);
|
||||||
@@ -414,18 +590,6 @@ class ZWaveJSConfigDashboard extends LitElement {
|
|||||||
this._dataCollectionOptIn =
|
this._dataCollectionOptIn =
|
||||||
dataCollectionStatus.opted_in === true ||
|
dataCollectionStatus.opted_in === true ||
|
||||||
dataCollectionStatus.enabled === true;
|
dataCollectionStatus.enabled === true;
|
||||||
|
|
||||||
this._fetchNodeStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _fetchNodeStatus() {
|
|
||||||
if (!this._network) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const nodeStatePromisses = this._network.controller.nodes.map((nodeId) =>
|
|
||||||
fetchZwaveNodeStatus(this.hass, this.configEntryId!, nodeId)
|
|
||||||
);
|
|
||||||
this._nodes = await Promise.all(nodeStatePromisses);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _addNodeClicked() {
|
private async _addNodeClicked() {
|
||||||
@@ -525,6 +689,11 @@ class ZWaveJSConfigDashboard extends LitElement {
|
|||||||
padding-right: 40px;
|
padding-right: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
.network-status div.heading {
|
.network-status div.heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -547,6 +716,10 @@ class ZWaveJSConfigDashboard extends LitElement {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mwc-list-item {
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
@@ -563,12 +736,6 @@ class ZWaveJSConfigDashboard extends LitElement {
|
|||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.dump {
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
[hidden] {
|
[hidden] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@@ -61,19 +61,6 @@ const getDevice = memoizeOne(
|
|||||||
entries?.find((device) => device.id === deviceId)
|
entries?.find((device) => device.id === deviceId)
|
||||||
);
|
);
|
||||||
|
|
||||||
const getNodeId = memoizeOne(
|
|
||||||
(device: DeviceRegistryEntry): number | undefined => {
|
|
||||||
const identifier = device.identifiers.find(
|
|
||||||
(ident) => ident[0] === "zwave_js"
|
|
||||||
);
|
|
||||||
if (!identifier) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseInt(identifier[1].split("-")[1]);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
@customElement("zwave_js-node-config")
|
@customElement("zwave_js-node-config")
|
||||||
class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
|
class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -179,17 +166,6 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
|
|||||||
</em>
|
</em>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
${this._nodeMetadata.comments?.length > 0
|
|
||||||
? html`
|
|
||||||
<div>
|
|
||||||
${this._nodeMetadata.comments.map(
|
|
||||||
(comment) => html`<ha-alert .alertType=${comment.level}>
|
|
||||||
${comment.text}
|
|
||||||
</ha-alert>`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ``}
|
|
||||||
<ha-card>
|
<ha-card>
|
||||||
${Object.entries(this._config).map(
|
${Object.entries(this._config).map(
|
||||||
([id, item]) => html` <ha-settings-row
|
([id, item]) => html` <ha-settings-row
|
||||||
@@ -382,12 +358,10 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _updateConfigParameter(target, value) {
|
private async _updateConfigParameter(target, value) {
|
||||||
const nodeId = getNodeId(this._device!);
|
|
||||||
try {
|
try {
|
||||||
const result = await setZwaveNodeConfigParameter(
|
const result = await setZwaveNodeConfigParameter(
|
||||||
this.hass,
|
this.hass,
|
||||||
this.configEntryId!,
|
this._device!.id,
|
||||||
nodeId!,
|
|
||||||
target.property,
|
target.property,
|
||||||
value,
|
value,
|
||||||
target.propertyKey ? target.propertyKey : undefined
|
target.propertyKey ? target.propertyKey : undefined
|
||||||
@@ -429,15 +403,9 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodeId = getNodeId(device);
|
|
||||||
if (!nodeId) {
|
|
||||||
this._error = "device_not_found";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
[this._nodeMetadata, this._config] = await Promise.all([
|
[this._nodeMetadata, this._config] = await Promise.all([
|
||||||
fetchZwaveNodeMetadata(this.hass, this.configEntryId, nodeId!),
|
fetchZwaveNodeMetadata(this.hass, device.id),
|
||||||
fetchZwaveNodeConfigParameters(this.hass, this.configEntryId, nodeId!),
|
fetchZwaveNodeConfigParameters(this.hass, device.id),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -87,7 +87,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
|||||||
${dashboard.default
|
${dashboard.default
|
||||||
? html`
|
? html`
|
||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
style="padding-left: 10px;"
|
style="padding-left: 10px; padding-inline-start: 10px; direction: var(--direction);"
|
||||||
.path=${mdiCheckCircleOutline}
|
.path=${mdiCheckCircleOutline}
|
||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
<paper-tooltip animation-delay="0">
|
<paper-tooltip animation-delay="0">
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user