mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-16 04:39:28 +00:00
Compare commits
93 Commits
entity-fil
...
20220203.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
51938fb51f | ||
![]() |
890ad9a1c8 | ||
![]() |
8466ef371a | ||
![]() |
4e55460799 | ||
![]() |
5fde6e659d | ||
![]() |
148bb99d89 | ||
![]() |
0540bae707 | ||
![]() |
0c6f647f53 | ||
![]() |
3aca67d511 | ||
![]() |
0e41a408e7 | ||
![]() |
19e1eaf2d7 | ||
![]() |
5e80a2b465 | ||
![]() |
866a57cde4 | ||
![]() |
c85236e251 | ||
![]() |
a88da0e39a | ||
![]() |
21a8fac477 | ||
![]() |
ca5ce04a38 | ||
![]() |
7c4b9a0410 | ||
![]() |
de6f06ea6d | ||
![]() |
bbc8e323e8 | ||
![]() |
89b6863ae3 | ||
![]() |
3f1850e9eb | ||
![]() |
54d6b5b6f3 | ||
![]() |
fb55ab197f | ||
![]() |
cc2db9a761 | ||
![]() |
58ba3e5c22 | ||
![]() |
182ffccd0c | ||
![]() |
ce99d14ee0 | ||
![]() |
8ce160b9ce | ||
![]() |
fe33714c8b | ||
![]() |
afbe85625c | ||
![]() |
cb47ee7721 | ||
![]() |
5caa256f1b | ||
![]() |
c66dfb84f9 | ||
![]() |
df1d703e4e | ||
![]() |
ce0ced0b6a | ||
![]() |
730e9b144d | ||
![]() |
69ff8dd0c4 | ||
![]() |
389a100b46 | ||
![]() |
9fee7a2829 | ||
![]() |
a91897821a | ||
![]() |
815a2a07ff | ||
![]() |
b8d3eb76ac | ||
![]() |
ba75c2e7af | ||
![]() |
f04b844223 | ||
![]() |
242bad0a29 | ||
![]() |
8b20b2b63c | ||
![]() |
e0c8efc5e6 | ||
![]() |
f59c30ac04 | ||
![]() |
e4b9c08b45 | ||
![]() |
04e63eefe2 | ||
![]() |
a064ca0856 | ||
![]() |
6044ea92ad | ||
![]() |
17e8215420 | ||
![]() |
a4ae1bee79 | ||
![]() |
7d335d7d85 | ||
![]() |
7c194d8910 | ||
![]() |
a92100bb0a | ||
![]() |
303af611d1 | ||
![]() |
559b6e9d5b | ||
![]() |
75a95ff675 | ||
![]() |
3024ee43f9 | ||
![]() |
b34b92fa87 | ||
![]() |
1832ed0a48 | ||
![]() |
f398692e75 | ||
![]() |
68bee4dd58 | ||
![]() |
f1297e1f36 | ||
![]() |
953e3e060b | ||
![]() |
c37f660718 | ||
![]() |
02754369a6 | ||
![]() |
0df9e9932f | ||
![]() |
eddb392ad0 | ||
![]() |
e8ba349447 | ||
![]() |
5be22d46ab | ||
![]() |
ffaff30b46 | ||
![]() |
069f08b55e | ||
![]() |
204ccf8b40 | ||
![]() |
0ab8f8fd7c | ||
![]() |
9b0b2c5b71 | ||
![]() |
0800c702fb | ||
![]() |
b7bd7c1065 | ||
![]() |
61bae5da64 | ||
![]() |
bdd13db8cf | ||
![]() |
cdc3d11181 | ||
![]() |
8f729e2a95 | ||
![]() |
bc9195f7d5 | ||
![]() |
7f1a321075 | ||
![]() |
72b9f8636d | ||
![]() |
c9cd316c0c | ||
![]() |
6cf3580fb4 | ||
![]() |
5d91aefb55 | ||
![]() |
e3c0530941 | ||
![]() |
2c9223ed80 |
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
||||
- name: Build and release package
|
||||
run: |
|
||||
python3 -m pip install twine
|
||||
python3 -m pip install twine build
|
||||
export TWINE_USERNAME="__token__"
|
||||
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
include README.md
|
||||
include LICENSE.md
|
||||
graft hass_frontend
|
||||
graft hass_frontend_es5
|
||||
recursive-exclude * *.py[co]
|
||||
|
@@ -26,11 +26,11 @@ module.exports = {
|
||||
},
|
||||
version() {
|
||||
const version = fs
|
||||
.readFileSync(path.resolve(paths.polymer_dir, "setup.py"), "utf8")
|
||||
.match(/\d{8}\.\d+/);
|
||||
.readFileSync(path.resolve(paths.polymer_dir, "setup.cfg"), "utf8")
|
||||
.match(/version\W+=\W(\d{8}\.\d)/);
|
||||
if (!version) {
|
||||
throw Error("Version not found");
|
||||
}
|
||||
return version[0];
|
||||
return version[1];
|
||||
},
|
||||
};
|
||||
|
@@ -188,6 +188,7 @@ class HaGallery extends LitElement {
|
||||
|
||||
.sidebar details {
|
||||
margin-top: 1em;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.sidebar summary {
|
||||
|
@@ -17,7 +17,7 @@ We want to make it as easy for designers to contribute as it is for developers.
|
||||
|
||||
- Meet us at <a href="https://discord.gg/BPBc8rZ9" rel="noopener noreferrer" target="_blank">devs_ux Discord</a>. Feel free to share your designs, user test or strategic ideas.
|
||||
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
|
||||
- Find the lates UX <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
|
||||
- Find the lates UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
|
||||
|
||||
|
||||
## Developers
|
||||
|
@@ -114,7 +114,7 @@ class HassioAddonConfig extends LitElement {
|
||||
<div class="card-menu">
|
||||
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("common.menu")}
|
||||
.label=${this.supervisor.localize("common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
|
@@ -191,7 +191,7 @@ export class HassioBackups extends LitElement {
|
||||
@action=${this._handleAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("common.menu")}
|
||||
.label=${this.supervisor?.localize("common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
|
@@ -17,27 +17,27 @@ export class DialogHassioBackupUpload
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _params?: HassioBackupUploadDialogParams;
|
||||
@state() private _dialogParams?: HassioBackupUploadDialogParams;
|
||||
|
||||
public async showDialog(
|
||||
params: HassioBackupUploadDialogParams
|
||||
dialogParams: HassioBackupUploadDialogParams
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
this._dialogParams = dialogParams;
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
if (this._params && !this._params.onboarding) {
|
||||
if (this._params.reloadBackup) {
|
||||
this._params.reloadBackup();
|
||||
if (this._dialogParams && !this._dialogParams.onboarding) {
|
||||
if (this._dialogParams.reloadBackup) {
|
||||
this._dialogParams.reloadBackup();
|
||||
}
|
||||
}
|
||||
this._params = undefined;
|
||||
this._dialogParams = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params) {
|
||||
if (!this._dialogParams) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
@@ -47,14 +47,20 @@ export class DialogHassioBackupUpload
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
.heading=${true}
|
||||
.heading=${this.hass?.localize(
|
||||
"ui.panel.page-onboarding.restore.upload_backup"
|
||||
) || "Upload backup"}
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<div slot="heading">
|
||||
<ha-header-bar>
|
||||
<span slot="title"> Upload backup </span>
|
||||
<span slot="title"
|
||||
>${this.hass?.localize(
|
||||
"ui.panel.page-onboarding.restore.upload_backup"
|
||||
) || "Upload backup"}</span
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this.hass?.localize("common.close") || "close"}
|
||||
.label=${this.hass?.localize("ui.common.close") || "Close"}
|
||||
.path=${mdiClose}
|
||||
slot="actionItems"
|
||||
dialogAction="cancel"
|
||||
@@ -71,7 +77,7 @@ export class DialogHassioBackupUpload
|
||||
|
||||
private _backupUploaded(ev) {
|
||||
const backup = ev.detail.backup;
|
||||
this._params?.showBackup(backup.slug);
|
||||
this._dialogParams?.showBackup(backup.slug);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
|
@@ -48,9 +48,9 @@ class HassioBackupDialog
|
||||
@query("supervisor-backup-content")
|
||||
private _backupContent!: SupervisorBackupContent;
|
||||
|
||||
public async showDialog(params: HassioBackupDialogParams) {
|
||||
this._backup = await fetchHassioBackupInfo(this.hass, params.slug);
|
||||
this._dialogParams = params;
|
||||
public async showDialog(dialogParams: HassioBackupDialogParams) {
|
||||
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug);
|
||||
this._dialogParams = dialogParams;
|
||||
this._restoringBackup = false;
|
||||
}
|
||||
|
||||
@@ -71,13 +71,13 @@ class HassioBackupDialog
|
||||
open
|
||||
scrimClickAction
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${true}
|
||||
.heading=${this._backup.name}
|
||||
>
|
||||
<div slot="heading">
|
||||
<ha-header-bar>
|
||||
<span slot="title">${this._backup.name}</span>
|
||||
<ha-icon-button
|
||||
.label=${this.hass?.localize("common.close") || "close"}
|
||||
.label=${this.hass?.localize("ui.common.close") || "Close"}
|
||||
.path=${mdiClose}
|
||||
slot="actionItems"
|
||||
dialogAction="cancel"
|
||||
@@ -114,12 +114,20 @@ class HassioBackupDialog
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize("common.menu")}
|
||||
.label=${this.hass!.localize("ui.common.menu") || "Menu"}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
<mwc-list-item>Download Backup</mwc-list-item>
|
||||
<mwc-list-item class="error">Delete Backup</mwc-list-item>
|
||||
<mwc-list-item
|
||||
>${this._dialogParams.supervisor?.localize(
|
||||
"backup.download_backup"
|
||||
)}</mwc-list-item
|
||||
>
|
||||
<mwc-list-item class="error"
|
||||
>${this._dialogParams.supervisor?.localize(
|
||||
"backup.delete_backup_title"
|
||||
)}</mwc-list-item
|
||||
>
|
||||
</ha-button-menu>`
|
||||
: ""}
|
||||
</ha-dialog>
|
||||
|
@@ -30,8 +30,8 @@ class HassioCreateBackupDialog extends LitElement {
|
||||
@query("supervisor-backup-content")
|
||||
private _backupContent!: SupervisorBackupContent;
|
||||
|
||||
public showDialog(params: HassioCreateBackupDialogParams) {
|
||||
this._dialogParams = params;
|
||||
public showDialog(dialogParams: HassioCreateBackupDialogParams) {
|
||||
this._dialogParams = dialogParams;
|
||||
this._creatingBackup = false;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ class HassioCreateBackupDialog extends LitElement {
|
||||
)}
|
||||
>
|
||||
${this._creatingBackup
|
||||
? html` <ha-circular-progress active></ha-circular-progress>`
|
||||
? html`<ha-circular-progress active></ha-circular-progress>`
|
||||
: html`<supervisor-backup-content
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this._dialogParams.supervisor}
|
||||
|
@@ -39,8 +39,8 @@ class HassioHardwareDialog extends LitElement {
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
public showDialog(params: HassioHardwareDialogParams) {
|
||||
this._dialogParams = params;
|
||||
public showDialog(dialogParams: HassioHardwareDialogParams) {
|
||||
this._dialogParams = dialogParams;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@@ -65,14 +65,16 @@ class HassioHardwareDialog extends LitElement {
|
||||
scrimClickAction
|
||||
hideActions
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${true}
|
||||
.heading=${this._dialogParams.supervisor.localize(
|
||||
"dialog.hardware.title"
|
||||
)}
|
||||
>
|
||||
<div class="header" slot="heading">
|
||||
<h2>
|
||||
${this._dialogParams.supervisor.localize("dialog.hardware.title")}
|
||||
</h2>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("common.close")}
|
||||
.label=${this._dialogParams.supervisor.localize("common.close")}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
></ha-icon-button>
|
||||
|
@@ -94,7 +94,7 @@ export class DialogHassioNetwork
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${true}
|
||||
.heading=${this.supervisor.localize("dialog.network.title")}
|
||||
hideActions
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
@@ -104,7 +104,7 @@ export class DialogHassioNetwork
|
||||
${this.supervisor.localize("dialog.network.title")}
|
||||
</span>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("common.close")}
|
||||
.label=${this.supervisor.localize("common.close")}
|
||||
.path=${mdiClose}
|
||||
slot="actionItems"
|
||||
dialogAction="cancel"
|
||||
|
@@ -186,7 +186,7 @@ class HassioHostInfo extends LitElement {
|
||||
|
||||
<ha-button-menu corner="BOTTOM_START">
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("common.menu")}
|
||||
.label=${this.supervisor.localize("common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
|
@@ -33,8 +33,12 @@ import {
|
||||
extractApiErrorMessage,
|
||||
ignoreSupervisorError,
|
||||
} from "../../../src/data/hassio/common";
|
||||
import { updateOS } from "../../../src/data/hassio/host";
|
||||
import { updateSupervisor } from "../../../src/data/hassio/supervisor";
|
||||
import { fetchHassioHassOsInfo, updateOS } from "../../../src/data/hassio/host";
|
||||
import {
|
||||
fetchHassioHomeAssistantInfo,
|
||||
fetchHassioSupervisorInfo,
|
||||
updateSupervisor,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import { updateCore } from "../../../src/data/supervisor/core";
|
||||
import { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
@@ -212,11 +216,22 @@ class UpdateAvailableCard extends LitElement {
|
||||
: "addon";
|
||||
this._updateType = updateType as updateType;
|
||||
|
||||
if (updateType === "addon") {
|
||||
if (!this.addonSlug) {
|
||||
this.addonSlug = pathPart;
|
||||
}
|
||||
this._loadAddonData();
|
||||
switch (updateType) {
|
||||
case "addon":
|
||||
if (!this.addonSlug) {
|
||||
this.addonSlug = pathPart;
|
||||
}
|
||||
this._loadAddonData();
|
||||
break;
|
||||
case "core":
|
||||
this._loadCoreData();
|
||||
break;
|
||||
case "supervisor":
|
||||
this._loadSupervisorData();
|
||||
break;
|
||||
case "os":
|
||||
this._loadOsData();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,6 +323,42 @@ class UpdateAvailableCard extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadSupervisorData() {
|
||||
try {
|
||||
const supervisor = await fetchHassioSupervisorInfo(this.hass);
|
||||
fireEvent(this, "supervisor-update", { supervisor });
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: this._updateType,
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadCoreData() {
|
||||
try {
|
||||
const core = await fetchHassioHomeAssistantInfo(this.hass);
|
||||
fireEvent(this, "supervisor-update", { core });
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: this._updateType,
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadOsData() {
|
||||
try {
|
||||
const os = await fetchHassioHassOsInfo(this.hass);
|
||||
fireEvent(this, "supervisor-update", { os });
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: this._updateType,
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _update() {
|
||||
this._error = undefined;
|
||||
this._updating = true;
|
||||
|
11
package.json
11
package.json
@@ -110,7 +110,7 @@
|
||||
"js-yaml": "^4.1.0",
|
||||
"leaflet": "^1.7.1",
|
||||
"leaflet-draw": "^1.0.4",
|
||||
"lit": "^2.0.2",
|
||||
"lit": "^2.1.2",
|
||||
"lit-vaadin-helpers": "^0.2.1",
|
||||
"marked": "^3.0.2",
|
||||
"memoize-one": "^5.2.1",
|
||||
@@ -168,6 +168,7 @@
|
||||
"@types/leaflet-draw": "^1",
|
||||
"@types/marked": "^2",
|
||||
"@types/mocha": "^8",
|
||||
"@types/qrcode": "^1.4.2",
|
||||
"@types/sortablejs": "^1",
|
||||
"@types/webspeechapi": "^0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "^4.32.0",
|
||||
@@ -235,10 +236,10 @@
|
||||
"resolutions": {
|
||||
"@polymer/polymer": "patch:@polymer/polymer@3.4.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
|
||||
"@webcomponents/webcomponentsjs": "^2.2.10",
|
||||
"lit": "^2.0.2",
|
||||
"lit-html": "2.0.1",
|
||||
"lit-element": "3.0.1",
|
||||
"@lit/reactive-element": "1.0.1"
|
||||
"lit": "^2.1.2",
|
||||
"lit-html": "2.1.2",
|
||||
"lit-element": "3.1.2",
|
||||
"@lit/reactive-element": "1.2.1"
|
||||
},
|
||||
"main": "src/home-assistant.js",
|
||||
"husky": {
|
||||
|
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[build-system]
|
||||
requires = ["setuptools~=60.5", "wheel~=0.37.1"]
|
||||
build-backend = "setuptools.build_meta"
|
@@ -11,6 +11,6 @@ yarn install
|
||||
|
||||
script/build_frontend
|
||||
|
||||
rm -rf dist
|
||||
python3 setup.py -q sdist
|
||||
python3 -m twine upload dist/* --skip-existing
|
||||
rm -rf dist home_assistant_frontend.egg-info
|
||||
python3 -m build
|
||||
python3 -m twine upload dist/*.whl --skip-existing
|
||||
|
@@ -50,14 +50,14 @@ async function main(args) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setup = fs.readFileSync("setup.py", "utf8");
|
||||
const setup = fs.readFileSync("setup.cfg", "utf8");
|
||||
const version = setup.match(/\d{8}\.\d+/)[0];
|
||||
const newVersion = method(version);
|
||||
|
||||
console.log("Current version:", version);
|
||||
console.log("New version:", newVersion);
|
||||
|
||||
fs.writeFileSync("setup.py", setup.replace(version, newVersion), "utf-8");
|
||||
fs.writeFileSync("setup.cfg", setup.replace(version, newVersion), "utf-8");
|
||||
|
||||
if (!commit) {
|
||||
return;
|
||||
|
21
setup.cfg
Normal file
21
setup.cfg
Normal file
@@ -0,0 +1,21 @@
|
||||
[metadata]
|
||||
name = home-assistant-frontend
|
||||
version = 20220203.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*
|
19
setup.py
19
setup.py
@@ -1,14 +1,7 @@
|
||||
from setuptools import setup, find_packages
|
||||
"""
|
||||
Entry point for setuptools. Required for editable installs.
|
||||
TODO: Remove file after updating to pip 21.3
|
||||
"""
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20220124.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/frontend",
|
||||
author="The Home Assistant Authors",
|
||||
author_email="hello@home-assistant.io",
|
||||
license="Apache-2.0",
|
||||
packages=find_packages(include=["hass_frontend", "hass_frontend.*"]),
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
)
|
||||
setup()
|
||||
|
@@ -184,6 +184,7 @@ export const DOMAINS_WITH_MORE_INFO = [
|
||||
"person",
|
||||
"remote",
|
||||
"script",
|
||||
"scene",
|
||||
"sun",
|
||||
"timer",
|
||||
"vacuum",
|
||||
@@ -234,7 +235,7 @@ export const DOMAINS_INPUT_ROW = [
|
||||
];
|
||||
|
||||
/** Domains that should have the history hidden in the more info dialog. */
|
||||
export const DOMAINS_MORE_INFO_NO_HISTORY = ["camera", "configurator", "scene"];
|
||||
export const DOMAINS_MORE_INFO_NO_HISTORY = ["camera", "configurator"];
|
||||
|
||||
/** States that we consider "off". */
|
||||
export const STATES_OFF = ["closed", "locked", "off"];
|
||||
|
@@ -13,14 +13,19 @@ export const formatDateTime = (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
|
||||
const formatDateTimeMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
})
|
||||
new Intl.DateTimeFormat(
|
||||
locale.language === "en" && !useAmPm(locale)
|
||||
? "en-u-hc-h23"
|
||||
: locale.language,
|
||||
{
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// August 9, 2021, 8:23:15 AM
|
||||
@@ -31,15 +36,20 @@ export const formatDateTimeWithSeconds = (
|
||||
|
||||
const formatDateTimeWithSecondsMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
})
|
||||
new Intl.DateTimeFormat(
|
||||
locale.language === "en" && !useAmPm(locale)
|
||||
? "en-u-hc-h23"
|
||||
: locale.language,
|
||||
{
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// 9/8/2021, 8:23 AM
|
||||
@@ -50,12 +60,17 @@ export const formatDateTimeNumeric = (
|
||||
|
||||
const formatDateTimeNumericMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
})
|
||||
new Intl.DateTimeFormat(
|
||||
locale.language === "en" && !useAmPm(locale)
|
||||
? "en-u-hc-h23"
|
||||
: locale.language,
|
||||
{
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
@@ -13,11 +13,16 @@ export const formatTime = (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
|
||||
const formatTimeMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
})
|
||||
new Intl.DateTimeFormat(
|
||||
locale.language === "en" && !useAmPm(locale)
|
||||
? "en-u-hc-h23"
|
||||
: locale.language,
|
||||
{
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// 9:15:24 PM || 21:15:24
|
||||
@@ -28,12 +33,17 @@ export const formatTimeWithSeconds = (
|
||||
|
||||
const formatTimeWithSecondsMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
})
|
||||
new Intl.DateTimeFormat(
|
||||
locale.language === "en" && !useAmPm(locale)
|
||||
? "en-u-hc-h23"
|
||||
: locale.language,
|
||||
{
|
||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Tuesday 7:00 PM || Tuesday 19:00
|
||||
@@ -42,10 +52,15 @@ export const formatTimeWeekday = (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
|
||||
const formatTimeWeekdayMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: "long",
|
||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
})
|
||||
new Intl.DateTimeFormat(
|
||||
locale.language === "en" && !useAmPm(locale)
|
||||
? "en-u-hc-h23"
|
||||
: locale.language,
|
||||
{
|
||||
weekday: "long",
|
||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
@@ -120,6 +120,7 @@ export const computeStateDisplay = (
|
||||
if (
|
||||
domain === "button" ||
|
||||
domain === "input_button" ||
|
||||
domain === "scene" ||
|
||||
(domain === "sensor" && stateObj.attributes.device_class === "timestamp")
|
||||
) {
|
||||
return formatDateTime(new Date(compareState), locale);
|
||||
|
@@ -1,2 +1,10 @@
|
||||
export const clamp = (value: number, min: number, max: number) =>
|
||||
Math.min(Math.max(value, min), max);
|
||||
|
||||
// Variant that only applies the clamping to a border if the border is defined
|
||||
export const conditionalClamp = (value: number, min?: number, max?: number) => {
|
||||
let result: number;
|
||||
result = min ? Math.max(value, min) : value;
|
||||
result = max ? Math.min(value, max) : value;
|
||||
return result;
|
||||
};
|
||||
|
@@ -77,7 +77,7 @@ export const computeLocalize = async (
|
||||
|
||||
await loadPolyfillLocales(language);
|
||||
|
||||
// Everytime any of the parameters change, invalidate the strings cache.
|
||||
// Every time any of the parameters change, invalidate the strings cache.
|
||||
cache._localizationCache = {};
|
||||
|
||||
return (key, ...args) => {
|
||||
|
@@ -68,6 +68,7 @@ export class HaProgressButton extends LitElement {
|
||||
--mdc-theme-primary: white;
|
||||
background-color: var(--success-color);
|
||||
transition: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
mwc-button[raised].success {
|
||||
@@ -79,6 +80,7 @@ export class HaProgressButton extends LitElement {
|
||||
--mdc-theme-primary: white;
|
||||
background-color: var(--error-color);
|
||||
transition: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
mwc-button[raised].error {
|
||||
|
@@ -183,12 +183,7 @@ class StateHistoryChartLine extends LitElement {
|
||||
prevValues = datavalues;
|
||||
};
|
||||
|
||||
const addDataSet = (
|
||||
nameY: string,
|
||||
step = false,
|
||||
fill = false,
|
||||
color?: string
|
||||
) => {
|
||||
const addDataSet = (nameY: string, fill = false, color?: string) => {
|
||||
if (!color) {
|
||||
color = getGraphColorByIndex(colorIndex, computedStyles);
|
||||
colorIndex++;
|
||||
@@ -198,7 +193,7 @@ class StateHistoryChartLine extends LitElement {
|
||||
fill: fill ? "origin" : false,
|
||||
borderColor: color,
|
||||
backgroundColor: color + "7F",
|
||||
stepped: step ? "before" : false,
|
||||
stepped: "before",
|
||||
pointRadius: 0,
|
||||
data: [],
|
||||
});
|
||||
@@ -239,14 +234,12 @@ class StateHistoryChartLine extends LitElement {
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.climate.current_temperature", {
|
||||
name: name,
|
||||
})}`,
|
||||
true
|
||||
})}`
|
||||
);
|
||||
if (hasHeat) {
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.climate.heating", { name: name })}`,
|
||||
true,
|
||||
true,
|
||||
computedStyles.getPropertyValue("--state-climate-heat-color")
|
||||
);
|
||||
// The "heating" series uses steppedArea to shade the area below the current
|
||||
@@ -256,7 +249,6 @@ class StateHistoryChartLine extends LitElement {
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.climate.cooling", { name: name })}`,
|
||||
true,
|
||||
true,
|
||||
computedStyles.getPropertyValue("--state-climate-cool-color")
|
||||
);
|
||||
// The "cooling" series uses steppedArea to shade the area below the current
|
||||
@@ -268,22 +260,19 @@ class StateHistoryChartLine extends LitElement {
|
||||
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
|
||||
name: name,
|
||||
mode: this.hass.localize("ui.card.climate.high"),
|
||||
})}`,
|
||||
true
|
||||
})}`
|
||||
);
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
|
||||
name: name,
|
||||
mode: this.hass.localize("ui.card.climate.low"),
|
||||
})}`,
|
||||
true
|
||||
})}`
|
||||
);
|
||||
} else {
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.climate.target_temperature_entity", {
|
||||
name: name,
|
||||
})}`,
|
||||
true
|
||||
})}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -318,14 +307,12 @@ class StateHistoryChartLine extends LitElement {
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.humidifier.target_humidity_entity", {
|
||||
name: name,
|
||||
})}`,
|
||||
true
|
||||
})}`
|
||||
);
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.humidifier.on_entity", {
|
||||
name: name,
|
||||
})}`,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
@@ -337,9 +324,7 @@ class StateHistoryChartLine extends LitElement {
|
||||
pushData(new Date(entityState.last_changed), series);
|
||||
});
|
||||
} else {
|
||||
// Only interpolate for sensors
|
||||
const isStep = domain !== "sensor";
|
||||
addDataSet(name, isStep);
|
||||
addDataSet(name);
|
||||
|
||||
let lastValue: number;
|
||||
let lastDate: Date;
|
||||
|
@@ -14,9 +14,9 @@ import {
|
||||
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { formatAttributeName } from "../../data/entity_attributes";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { formatAttributeName } from "../../util/hass-attributes-util";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "./state-badge";
|
||||
|
@@ -12,7 +12,7 @@ import { property, state } from "lit/decorators";
|
||||
import { STATES_OFF } from "../../common/const";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { UNAVAILABLE, UNAVAILABLE_STATES } from "../../data/entity";
|
||||
import { UNAVAILABLE, UNAVAILABLE_STATES, UNKNOWN } from "../../data/entity";
|
||||
import { forwardHaptic } from "../../data/haptics";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-formfield";
|
||||
@@ -39,21 +39,26 @@ export class HaEntityToggle extends LitElement {
|
||||
return html` <ha-switch disabled></ha-switch> `;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.assumed_state) {
|
||||
if (
|
||||
this.stateObj.attributes.assumed_state ||
|
||||
this.stateObj.state === UNKNOWN
|
||||
) {
|
||||
return html`
|
||||
<ha-icon-button
|
||||
.label=${`Turn ${computeStateName(this.stateObj)} off`}
|
||||
.path=${mdiFlashOff}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
@click=${this._turnOff}
|
||||
?state-active=${!this._isOn}
|
||||
class=${!this._isOn && this.stateObj.state !== UNKNOWN
|
||||
? "state-active"
|
||||
: ""}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.label=${`Turn ${computeStateName(this.stateObj)} on`}
|
||||
.path=${mdiFlash}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
@click=${this._turnOn}
|
||||
?state-active=${this._isOn}
|
||||
class=${this._isOn ? "state-active" : ""}
|
||||
></ha-icon-button>
|
||||
`;
|
||||
}
|
||||
@@ -63,7 +68,7 @@ export class HaEntityToggle extends LitElement {
|
||||
this._isOn ? "off" : "on"
|
||||
}`}
|
||||
.checked=${this._isOn}
|
||||
.disabled=${UNAVAILABLE_STATES.includes(this.stateObj.state)}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
@change=${this._toggleChanged}
|
||||
></ha-switch>`;
|
||||
|
||||
@@ -156,10 +161,11 @@ export class HaEntityToggle extends LitElement {
|
||||
min-width: 38px;
|
||||
}
|
||||
ha-icon-button {
|
||||
--mdc-icon-button-size: 40px;
|
||||
color: var(--ha-icon-button-inactive-color, var(--primary-text-color));
|
||||
transition: color 0.5s;
|
||||
}
|
||||
ha-icon-button[state-active] {
|
||||
ha-icon-button.state-active {
|
||||
color: var(--ha-icon-button-active-color, var(--primary-color));
|
||||
}
|
||||
ha-switch {
|
||||
|
@@ -1,12 +1,14 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import { HomeAssistant } from "../types";
|
||||
import hassAttributeUtil, {
|
||||
import {
|
||||
formatAttributeName,
|
||||
formatAttributeValue,
|
||||
} from "../util/hass-attributes-util";
|
||||
STATE_ATTRIBUTES,
|
||||
} from "../data/entity_attributes";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
import "./ha-expansion-panel";
|
||||
|
||||
@customElement("ha-attributes")
|
||||
@@ -25,7 +27,7 @@ class HaAttributes extends LitElement {
|
||||
}
|
||||
|
||||
const attributes = this.computeDisplayAttributes(
|
||||
Object.keys(hassAttributeUtil.LOGIC_STATE_ATTRIBUTES).concat(
|
||||
STATE_ATTRIBUTES.concat(
|
||||
this.extraFilters ? this.extraFilters.split(",") : []
|
||||
)
|
||||
);
|
||||
|
@@ -1,141 +0,0 @@
|
||||
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { EventsMixin } from "../mixins/events-mixin";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HaClimateControl extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex iron-flex-alignment"></style>
|
||||
<style>
|
||||
/* local DOM styles go here */
|
||||
:host {
|
||||
@apply --layout-flex;
|
||||
@apply --layout-horizontal;
|
||||
@apply --layout-justified;
|
||||
}
|
||||
.in-flux#target_temperature {
|
||||
color: var(--error-color);
|
||||
}
|
||||
#target_temperature {
|
||||
@apply --layout-self-center;
|
||||
font-size: 200%;
|
||||
direction: ltr;
|
||||
}
|
||||
.control-buttons {
|
||||
font-size: 200%;
|
||||
text-align: right;
|
||||
}
|
||||
ha-icon-button {
|
||||
--mdc-icon-size: 32px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- local DOM goes here -->
|
||||
<div id="target_temperature">[[value]] [[units]]</div>
|
||||
<div class="control-buttons">
|
||||
<div>
|
||||
<ha-icon-button on-click="incrementValue">
|
||||
<ha-icon icon="hass:chevron-up"></ha-icon>
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
<div>
|
||||
<ha-icon-button on-click="decrementValue">
|
||||
<ha-icon icon="hass:chevron-down"></ha-icon>
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
value: {
|
||||
type: Number,
|
||||
observer: "valueChanged",
|
||||
},
|
||||
units: {
|
||||
type: String,
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
value: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
temperatureStateInFlux(inFlux) {
|
||||
this.$.target_temperature.classList.toggle("in-flux", inFlux);
|
||||
}
|
||||
|
||||
_round(val) {
|
||||
// round value to precision derived from step
|
||||
// insired by https://github.com/soundar24/roundSlider/blob/master/src/roundslider.js
|
||||
const s = this.step.toString().split(".");
|
||||
return s[1] ? parseFloat(val.toFixed(s[1].length)) : Math.round(val);
|
||||
}
|
||||
|
||||
incrementValue() {
|
||||
const newval = this._round(this.value + this.step);
|
||||
if (this.value < this.max) {
|
||||
this.last_changed = Date.now();
|
||||
this.temperatureStateInFlux(true);
|
||||
}
|
||||
if (newval <= this.max) {
|
||||
// If no initial target_temp
|
||||
// this forces control to start
|
||||
// from the min configured instead of 0
|
||||
if (newval <= this.min) {
|
||||
this.value = this.min;
|
||||
} else {
|
||||
this.value = newval;
|
||||
}
|
||||
} else {
|
||||
this.value = this.max;
|
||||
}
|
||||
}
|
||||
|
||||
decrementValue() {
|
||||
const newval = this._round(this.value - this.step);
|
||||
if (this.value > this.min) {
|
||||
this.last_changed = Date.now();
|
||||
this.temperatureStateInFlux(true);
|
||||
}
|
||||
if (newval >= this.min) {
|
||||
this.value = newval;
|
||||
} else {
|
||||
this.value = this.min;
|
||||
}
|
||||
}
|
||||
|
||||
valueChanged() {
|
||||
// when the last_changed timestamp is changed,
|
||||
// trigger a potential event fire in
|
||||
// the future, as long as last changed is far enough in the
|
||||
// past.
|
||||
if (this.last_changed) {
|
||||
window.setTimeout(() => {
|
||||
const now = Date.now();
|
||||
if (now - this.last_changed >= 2000) {
|
||||
this.fire("change");
|
||||
this.temperatureStateInFlux(false);
|
||||
this.last_changed = null;
|
||||
}
|
||||
}, 2010);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-climate-control", HaClimateControl);
|
138
src/components/ha-climate-control.ts
Normal file
138
src/components/ha-climate-control.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { mdiChevronDown, mdiChevronUp } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { conditionalClamp } from "../common/number/clamp";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@customElement("ha-climate-control")
|
||||
class HaClimateControl extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public value!: number;
|
||||
|
||||
@property() public unit = "";
|
||||
|
||||
@property() public min?: number;
|
||||
|
||||
@property() public max?: number;
|
||||
|
||||
@property() public step = 1;
|
||||
|
||||
private _lastChanged?: number;
|
||||
|
||||
@query("#target_temperature") private _targetTemperature!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div id="target_temperature">${this.value} ${this.unit}</div>
|
||||
<div class="control-buttons">
|
||||
<div>
|
||||
<ha-icon-button
|
||||
.path=${mdiChevronUp}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.climate-control.temperature_up"
|
||||
)}
|
||||
@click=${this._incrementValue}
|
||||
>
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
<div>
|
||||
<ha-icon-button
|
||||
.path=${mdiChevronDown}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.climate-control.temperature_down"
|
||||
)}
|
||||
@click=${this._decrementValue}
|
||||
>
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProperties) {
|
||||
if (changedProperties.has("value")) {
|
||||
this._valueChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private _temperatureStateInFlux(inFlux) {
|
||||
this._targetTemperature.classList.toggle("in-flux", inFlux);
|
||||
}
|
||||
|
||||
private _round(value) {
|
||||
// Round value to precision derived from step.
|
||||
// Inspired by https://github.com/soundar24/roundSlider/blob/master/src/roundslider.js
|
||||
const s = this.step.toString().split(".");
|
||||
return s[1] ? parseFloat(value.toFixed(s[1].length)) : Math.round(value);
|
||||
}
|
||||
|
||||
private _incrementValue() {
|
||||
const newValue = this._round(this.value + this.step);
|
||||
this._processNewValue(newValue);
|
||||
}
|
||||
|
||||
private _decrementValue() {
|
||||
const newValue = this._round(this.value - this.step);
|
||||
this._processNewValue(newValue);
|
||||
}
|
||||
|
||||
private _processNewValue(value) {
|
||||
const newValue = conditionalClamp(value, this.min, this.max);
|
||||
|
||||
if (this.value !== newValue) {
|
||||
this.value = newValue;
|
||||
this._lastChanged = Date.now();
|
||||
this._temperatureStateInFlux(true);
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChanged() {
|
||||
// When the last_changed timestamp is changed,
|
||||
// trigger a potential event fire in the future,
|
||||
// as long as last_changed is far enough in the past.
|
||||
if (this._lastChanged) {
|
||||
window.setTimeout(() => {
|
||||
const now = Date.now();
|
||||
if (now - this._lastChanged! >= 2000) {
|
||||
fireEvent(this, "change");
|
||||
this._temperatureStateInFlux(false);
|
||||
this._lastChanged = undefined;
|
||||
}
|
||||
}, 2010);
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.in-flux {
|
||||
color: var(--error-color);
|
||||
}
|
||||
#target_temperature {
|
||||
align-self: center;
|
||||
font-size: 28px;
|
||||
direction: ltr;
|
||||
}
|
||||
.control-buttons {
|
||||
font-size: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
ha-icon-button {
|
||||
--mdc-icon-size: 32px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-climate-control": HaClimateControl;
|
||||
}
|
||||
}
|
@@ -1,6 +1,5 @@
|
||||
import "@material/mwc-textfield";
|
||||
import type { TextField } from "@material/mwc-textfield";
|
||||
import "@material/mwc-slider";
|
||||
import type { Slider } from "@material/mwc-slider";
|
||||
import {
|
||||
css,
|
||||
@@ -14,6 +13,7 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HaCheckbox } from "../ha-checkbox";
|
||||
import { HaFormElement, HaFormIntegerData, HaFormIntegerSchema } from "./types";
|
||||
import "../ha-slider";
|
||||
|
||||
@customElement("ha-form-integer")
|
||||
export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
@@ -54,15 +54,16 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
></ha-checkbox>
|
||||
`
|
||||
: ""}
|
||||
<mwc-slider
|
||||
discrete
|
||||
<ha-slider
|
||||
pin
|
||||
ignore-bar-touch
|
||||
.value=${this._value}
|
||||
.min=${this.schema.valueMin}
|
||||
.max=${this.schema.valueMax}
|
||||
.disabled=${this.disabled ||
|
||||
(this.data === undefined && this.schema.optional)}
|
||||
@change=${this._valueChanged}
|
||||
></mwc-slider>
|
||||
></ha-slider>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -168,7 +169,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
mwc-slider {
|
||||
ha-slider {
|
||||
flex: 1;
|
||||
}
|
||||
mwc-textfield {
|
||||
|
@@ -104,7 +104,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
return css`
|
||||
.root {
|
||||
margin-bottom: -24px;
|
||||
overflow: auto;
|
||||
overflow: clip visible;
|
||||
}
|
||||
.root > * {
|
||||
display: block;
|
||||
|
@@ -17,6 +17,7 @@ import {
|
||||
import { Selector } from "../data/selector";
|
||||
import { PolymerChangedEvent } from "../polymer-types";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import "./ha-checkbox";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-selector/ha-selector";
|
||||
@@ -230,7 +231,12 @@ export class HaServiceControl extends LitElement {
|
||||
<p>${serviceData?.description}</p>
|
||||
${this._manifest
|
||||
? html` <a
|
||||
href=${this._manifest.documentation}
|
||||
href=${this._manifest.is_built_in
|
||||
? documentationUrl(
|
||||
this.hass,
|
||||
`/integrations/${this._manifest.domain}`
|
||||
)
|
||||
: this._manifest.documentation}
|
||||
title=${this.hass.localize(
|
||||
"ui.components.service-control.integration_doc"
|
||||
)}
|
||||
|
@@ -1,9 +1,13 @@
|
||||
import "../ha-header-bar";
|
||||
import { mdiArrowLeft, mdiClose } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
||||
import type {
|
||||
MediaPickedEvent,
|
||||
MediaPlayerBrowseAction,
|
||||
MediaPlayerItem,
|
||||
} from "../../data/media-player";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -16,6 +20,8 @@ import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog";
|
||||
class DialogMediaPlayerBrowse extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _currentItem?: MediaPlayerItem;
|
||||
|
||||
@state() private _navigateIds?: MediaPlayerItemId[];
|
||||
|
||||
@state() private _params?: MediaPlayerBrowseDialogParams;
|
||||
@@ -33,11 +39,12 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
public closeDialog() {
|
||||
this._params = undefined;
|
||||
this._navigateIds = undefined;
|
||||
this._currentItem = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params) {
|
||||
if (!this._params || !this._navigateIds) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
@@ -48,8 +55,40 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
flexContent
|
||||
.heading=${!this._currentItem
|
||||
? this.hass.localize(
|
||||
"ui.components.media-browser.media-player-browser"
|
||||
)
|
||||
: this._currentItem.title}
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<ha-header-bar slot="heading">
|
||||
${this._navigateIds.length > 1
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.path=${mdiArrowLeft}
|
||||
@click=${this._goBack}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
<span slot="title">
|
||||
${!this._currentItem
|
||||
? this.hass.localize(
|
||||
"ui.components.media-browser.media-player-browser"
|
||||
)
|
||||
: this._currentItem.title}
|
||||
</span>
|
||||
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.dialogs.generic.close")}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
slot="actionItems"
|
||||
class="header_button"
|
||||
dir=${computeRTLDirection(this.hass)}
|
||||
></ha-icon-button>
|
||||
</ha-header-bar>
|
||||
<ha-media-player-browse
|
||||
dialog
|
||||
.hass=${this.hass}
|
||||
@@ -64,8 +103,14 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _mediaBrowsed(ev) {
|
||||
private _goBack() {
|
||||
this._navigateIds = this._navigateIds?.slice(0, -1);
|
||||
this._currentItem = undefined;
|
||||
}
|
||||
|
||||
private _mediaBrowsed(ev: { detail: HASSDomEvents["media-browsed"] }) {
|
||||
this._navigateIds = ev.detail.ids;
|
||||
this._currentItem = ev.detail.current;
|
||||
}
|
||||
|
||||
private _mediaPicked(ev: HASSDomEvent<MediaPickedEvent>): void {
|
||||
@@ -89,7 +134,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
ha-media-player-browse {
|
||||
--media-browser-max-height: 100vh;
|
||||
--media-browser-max-height: calc(100vh - 65px);
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
@@ -101,10 +146,17 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
ha-media-player-browse {
|
||||
position: initial;
|
||||
--media-browser-max-height: 100vh - 72px;
|
||||
--media-browser-max-height: 100vh - 137px;
|
||||
width: 700px;
|
||||
}
|
||||
}
|
||||
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -51,11 +51,13 @@ export interface CloudStatusLoggedIn {
|
||||
google_registered: boolean;
|
||||
google_entities: EntityFilter;
|
||||
google_domains: string[];
|
||||
alexa_registered: boolean;
|
||||
alexa_entities: EntityFilter;
|
||||
prefs: CloudPreferences;
|
||||
remote_domain: string | undefined;
|
||||
remote_connected: boolean;
|
||||
remote_certificate: undefined | CertificateInformation;
|
||||
http_use_ssl: boolean;
|
||||
}
|
||||
|
||||
export type CloudStatus = CloudStatusNotLoggedIn | CloudStatusLoggedIn;
|
||||
|
@@ -104,18 +104,19 @@ export const localizeConfigFlowTitle = (
|
||||
localize: LocalizeFunc,
|
||||
flow: DataEntryFlowProgress
|
||||
) => {
|
||||
const placeholders = flow.context.title_placeholders || {};
|
||||
const placeholderKeys = Object.keys(placeholders);
|
||||
if (placeholderKeys.length === 0) {
|
||||
if (
|
||||
!flow.context.title_placeholders ||
|
||||
Object.keys(flow.context.title_placeholders).length === 0
|
||||
) {
|
||||
return domainToName(localize, flow.handler);
|
||||
}
|
||||
const args: string[] = [];
|
||||
placeholderKeys.forEach((key) => {
|
||||
args.push(key);
|
||||
args.push(placeholders[key]);
|
||||
});
|
||||
return localize(`component.${flow.handler}.config.flow_title`, ...args) ||
|
||||
"name" in placeholders
|
||||
? placeholders.name
|
||||
: domainToName(localize, flow.handler);
|
||||
return (
|
||||
localize(
|
||||
`component.${flow.handler}.config.flow_title`,
|
||||
flow.context.title_placeholders
|
||||
) ||
|
||||
("name" in flow.context.title_placeholders
|
||||
? flow.context.title_placeholders.name
|
||||
: domainToName(localize, flow.handler))
|
||||
);
|
||||
};
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { EntityRegistryEntry } from "./entity_registry";
|
||||
@@ -54,7 +55,13 @@ export const computeDeviceName = (
|
||||
device.name_by_user ||
|
||||
device.name ||
|
||||
(entities && fallbackDeviceName(hass, entities)) ||
|
||||
hass.localize("ui.panel.config.devices.unnamed_device");
|
||||
hass.localize(
|
||||
"ui.panel.config.devices.unnamed_device",
|
||||
"type",
|
||||
hass.localize(
|
||||
`ui.panel.config.devices.type.${device.entry_type || "device"}`
|
||||
)
|
||||
);
|
||||
|
||||
export const devicesInArea = (devices: DeviceRegistryEntry[], areaId: string) =>
|
||||
devices.filter((device) => device.area_id === areaId);
|
||||
@@ -99,3 +106,8 @@ export const subscribeDeviceRegistry = (
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
||||
export const sortDeviceRegistryByName = (entries: DeviceRegistryEntry[]) =>
|
||||
entries.sort((entry1, entry2) =>
|
||||
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "")
|
||||
);
|
||||
|
105
src/data/entity_attributes.ts
Normal file
105
src/data/entity_attributes.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { html, TemplateResult } from "lit";
|
||||
import { until } from "lit/directives/until";
|
||||
import checkValidDate from "../common/datetime/check_valid_date";
|
||||
import { formatDate } from "../common/datetime/format_date";
|
||||
import { formatDateTimeWithSeconds } from "../common/datetime/format_date_time";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter";
|
||||
import { isDate } from "../common/string/is_date";
|
||||
import { isTimestamp } from "../common/string/is_timestamp";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
let jsYamlPromise: Promise<typeof import("../resources/js-yaml-dump")>;
|
||||
|
||||
export const STATE_ATTRIBUTES = [
|
||||
"assumed_state",
|
||||
"attribution",
|
||||
"custom_ui_more_info",
|
||||
"custom_ui_state_card",
|
||||
"device_class",
|
||||
"editable",
|
||||
"emulated_hue_name",
|
||||
"emulated_hue",
|
||||
"entity_picture",
|
||||
"friendly_name",
|
||||
"haaska_hidden",
|
||||
"haaska_name",
|
||||
"icon",
|
||||
"initial_state",
|
||||
"last_reset",
|
||||
"restored",
|
||||
"state_class",
|
||||
"supported_features",
|
||||
"unit_of_measurement",
|
||||
];
|
||||
|
||||
// Convert from internal snake_case format to user-friendly format
|
||||
export function formatAttributeName(value: string): string {
|
||||
value = value
|
||||
.replace(/_/g, " ")
|
||||
.replace(/\bid\b/g, "ID")
|
||||
.replace(/\bip\b/g, "IP")
|
||||
.replace(/\bmac\b/g, "MAC")
|
||||
.replace(/\bgps\b/g, "GPS");
|
||||
return capitalizeFirstLetter(value);
|
||||
}
|
||||
|
||||
export function formatAttributeValue(
|
||||
hass: HomeAssistant,
|
||||
value: any
|
||||
): string | TemplateResult {
|
||||
if (value === null) {
|
||||
return "—";
|
||||
}
|
||||
|
||||
// YAML handling
|
||||
if (
|
||||
(Array.isArray(value) && value.some((val) => val instanceof Object)) ||
|
||||
(!Array.isArray(value) && value instanceof Object)
|
||||
) {
|
||||
if (!jsYamlPromise) {
|
||||
jsYamlPromise = import("../resources/js-yaml-dump");
|
||||
}
|
||||
const yaml = jsYamlPromise.then((jsYaml) => jsYaml.dump(value));
|
||||
return html`<pre>${until(yaml, "")}</pre>`;
|
||||
}
|
||||
|
||||
if (typeof value === "number") {
|
||||
return formatNumber(value, hass.locale);
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
// URL handling
|
||||
if (value.startsWith("http")) {
|
||||
try {
|
||||
// If invalid URL, exception will be raised
|
||||
const url = new URL(value);
|
||||
if (url.protocol === "http:" || url.protocol === "https:")
|
||||
return html`<a target="_blank" rel="noreferrer" href=${value}
|
||||
>${value}</a
|
||||
>`;
|
||||
} catch (_) {
|
||||
// Nothing to do here
|
||||
}
|
||||
}
|
||||
|
||||
// Date handling
|
||||
if (isDate(value, true)) {
|
||||
// Timestamp handling
|
||||
if (isTimestamp(value)) {
|
||||
const date = new Date(value);
|
||||
if (checkValidDate(date)) {
|
||||
return formatDateTimeWithSeconds(date, hass.locale);
|
||||
}
|
||||
}
|
||||
|
||||
// Value was not a timestamp, so only do date formatting
|
||||
const date = new Date(value);
|
||||
if (checkValidDate(date)) {
|
||||
return formatDate(date, hass.locale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.isArray(value) ? value.join(", ") : value;
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||
import { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
@@ -133,3 +134,8 @@ export const subscribeEntityRegistry = (
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
||||
export const sortEntityRegistryByName = (entries: EntityRegistryEntry[]) =>
|
||||
entries.sort((entry1, entry2) =>
|
||||
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "")
|
||||
);
|
||||
|
@@ -8,3 +8,6 @@ export interface GoogleEntity {
|
||||
|
||||
export const fetchCloudGoogleEntities = (hass: HomeAssistant) =>
|
||||
hass.callWS<GoogleEntity[]>({ type: "cloud/google_assistant/entities" });
|
||||
|
||||
export const syncCloudGoogleEntities = (hass: HomeAssistant) =>
|
||||
hass.callApi("POST", "cloud/google_actions/sync");
|
||||
|
@@ -88,7 +88,7 @@ export const BROWSER_PLAYER = "browser";
|
||||
export type MediaClassBrowserSetting = {
|
||||
icon: string;
|
||||
thumbnail_ratio?: string;
|
||||
layout?: string;
|
||||
layout?: "grid";
|
||||
show_list_images?: boolean;
|
||||
};
|
||||
|
||||
@@ -185,15 +185,6 @@ export const browseMediaPlayer = (
|
||||
media_content_type: mediaContentType,
|
||||
});
|
||||
|
||||
export const browseLocalMediaPlayer = (
|
||||
hass: HomeAssistant,
|
||||
mediaContentId?: string
|
||||
): Promise<MediaPlayerItem> =>
|
||||
hass.callWS<MediaPlayerItem>({
|
||||
type: "media_source/browse_media",
|
||||
media_content_id: mediaContentId,
|
||||
});
|
||||
|
||||
export const getCurrentProgress = (stateObj: MediaPlayerEntity): number => {
|
||||
let progress = stateObj.attributes.media_position!;
|
||||
|
||||
@@ -321,8 +312,8 @@ export const computeMediaControls = (
|
||||
return buttons.length > 0 ? buttons : undefined;
|
||||
};
|
||||
|
||||
export const formatMediaTime = (seconds: number): string => {
|
||||
if (!seconds) {
|
||||
export const formatMediaTime = (seconds: number | undefined): string => {
|
||||
if (seconds === undefined) {
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
import { MediaPlayerItem } from "./media-player";
|
||||
|
||||
export interface ResolvedMediaSource {
|
||||
url: string;
|
||||
@@ -13,3 +14,12 @@ export const resolveMediaSource = (
|
||||
type: "media_source/resolve_media",
|
||||
media_content_id,
|
||||
});
|
||||
|
||||
export const browseLocalMediaPlayer = (
|
||||
hass: HomeAssistant,
|
||||
mediaContentId?: string
|
||||
): Promise<MediaPlayerItem> =>
|
||||
hass.callWS<MediaPlayerItem>({
|
||||
type: "media_source/browse_media",
|
||||
media_content_id: mediaContentId,
|
||||
});
|
||||
|
@@ -82,7 +82,7 @@ async function processEvent(
|
||||
event: SupervisorEvent,
|
||||
key: string
|
||||
) {
|
||||
if (event.event !== "supervisor-update" || event.update_key !== key) {
|
||||
if (event.event !== "supervisor_update" || event.update_key !== key) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -116,15 +116,14 @@ class DataEntryFlowDialog extends LitElement {
|
||||
params.continueFlowId
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._step = undefined;
|
||||
this._params = undefined;
|
||||
this.closeDialog();
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.error"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
text: `${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.could_not_load"
|
||||
),
|
||||
)}: ${err.message || err.body}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -177,6 +176,7 @@ class DataEntryFlowDialog extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
this._loading = undefined;
|
||||
this._step = undefined;
|
||||
this._params = undefined;
|
||||
this._devices = undefined;
|
||||
@@ -372,15 +372,14 @@ class DataEntryFlowDialog extends LitElement {
|
||||
try {
|
||||
step = await this._params!.flowConfig.createFlow(this.hass, handler);
|
||||
} catch (err: any) {
|
||||
this._step = undefined;
|
||||
this._params = undefined;
|
||||
this.closeDialog();
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.error"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
text: `${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.could_not_load"
|
||||
),
|
||||
)}: ${err.message || err.body}`,
|
||||
});
|
||||
return;
|
||||
} finally {
|
||||
@@ -405,6 +404,15 @@ class DataEntryFlowDialog extends LitElement {
|
||||
this._loading = "loading_step";
|
||||
try {
|
||||
this._step = await step;
|
||||
} catch (err: any) {
|
||||
this.closeDialog();
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.error"
|
||||
),
|
||||
text: err.message || err.body,
|
||||
});
|
||||
return;
|
||||
} finally {
|
||||
this._loading = undefined;
|
||||
}
|
||||
|
@@ -26,6 +26,7 @@ interface ShowDialogParams<T> {
|
||||
dialogTag: keyof HTMLElementTagNameMap;
|
||||
dialogImport: () => Promise<unknown>;
|
||||
dialogParams: T;
|
||||
addHistory?: boolean;
|
||||
}
|
||||
|
||||
export interface DialogClosedParams {
|
||||
@@ -124,8 +125,15 @@ export const makeDialogManager = (
|
||||
element.addEventListener(
|
||||
"show-dialog",
|
||||
(e: HASSDomEvent<ShowDialogParams<unknown>>) => {
|
||||
const { dialogTag, dialogImport, dialogParams } = e.detail;
|
||||
showDialog(element, root, dialogTag, dialogParams, dialogImport);
|
||||
const { dialogTag, dialogImport, dialogParams, addHistory } = e.detail;
|
||||
showDialog(
|
||||
element,
|
||||
root,
|
||||
dialogTag,
|
||||
dialogParams,
|
||||
dialogImport,
|
||||
addHistory
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@@ -103,8 +103,9 @@ class MoreInfoClimate extends LitElement {
|
||||
stateObj.attributes.temperature !== null
|
||||
? html`
|
||||
<ha-climate-control
|
||||
.hass=${this.hass}
|
||||
.value=${stateObj.attributes.temperature}
|
||||
.units=${hass.config.unit_system.temperature}
|
||||
.unit=${hass.config.unit_system.temperature}
|
||||
.step=${temperatureStepSize}
|
||||
.min=${stateObj.attributes.min_temp}
|
||||
.max=${stateObj.attributes.max_temp}
|
||||
@@ -118,8 +119,9 @@ class MoreInfoClimate extends LitElement {
|
||||
stateObj.attributes.target_temp_high !== null)
|
||||
? html`
|
||||
<ha-climate-control
|
||||
.hass=${this.hass}
|
||||
.value=${stateObj.attributes.target_temp_low}
|
||||
.units=${hass.config.unit_system.temperature}
|
||||
.unit=${hass.config.unit_system.temperature}
|
||||
.step=${temperatureStepSize}
|
||||
.min=${stateObj.attributes.min_temp}
|
||||
.max=${stateObj.attributes.target_temp_high}
|
||||
@@ -127,8 +129,9 @@ class MoreInfoClimate extends LitElement {
|
||||
@change=${this._targetTemperatureLowChanged}
|
||||
></ha-climate-control>
|
||||
<ha-climate-control
|
||||
.hass=${this.hass}
|
||||
.value=${stateObj.attributes.target_temp_high}
|
||||
.units=${hass.config.unit_system.temperature}
|
||||
.unit=${hass.config.unit_system.temperature}
|
||||
.step=${temperatureStepSize}
|
||||
.min=${stateObj.attributes.target_temp_low}
|
||||
.max=${stateObj.attributes.max_temp}
|
||||
|
@@ -105,7 +105,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${true}
|
||||
.heading=${name}
|
||||
hideActions
|
||||
data-domain=${domain}
|
||||
>
|
||||
|
@@ -108,7 +108,9 @@ export class QuickBar extends LitElement {
|
||||
public async showDialog(params: QuickBarParams) {
|
||||
this._commandMode = params.commandMode || this._toggleIfAlreadyOpened();
|
||||
this._hint = params.hint;
|
||||
this._narrow = matchMedia("(max-width: 600px)").matches;
|
||||
this._narrow = matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
this._initializeItemsIfNeeded();
|
||||
this._opened = true;
|
||||
}
|
||||
@@ -137,7 +139,7 @@ export class QuickBar extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.heading=${true}
|
||||
.heading=${this.hass.localize("ui.dialogs.quick-bar.title")}
|
||||
open
|
||||
@opened=${this._handleOpened}
|
||||
@closed=${this.closeDialog}
|
||||
@@ -154,7 +156,7 @@ export class QuickBar extends LitElement {
|
||||
)}
|
||||
.value=${this._commandMode ? `>${this._search}` : this._search}
|
||||
.icon=${true}
|
||||
.iconTrailing=${this._search !== undefined}
|
||||
.iconTrailing=${this._search !== undefined || this._narrow}
|
||||
@input=${this._handleSearchChange}
|
||||
@keydown=${this._handleInputKeyDown}
|
||||
@focus=${this._setFocusFirstListItem}
|
||||
@@ -174,24 +176,27 @@ export class QuickBar extends LitElement {
|
||||
.path=${mdiMagnify}
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
${this._search &&
|
||||
html`
|
||||
<ha-icon-button
|
||||
slot="trailingIcon"
|
||||
@click=${this._clearSearch}
|
||||
.label=${this.hass!.localize("ui.common.clear")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
`}
|
||||
${this._search || this._narrow
|
||||
? html`
|
||||
<div slot="trailingIcon">
|
||||
${this._search &&
|
||||
html`<ha-icon-button
|
||||
@click=${this._clearSearch}
|
||||
.label=${this.hass!.localize("ui.common.clear")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>`}
|
||||
${this._narrow
|
||||
? html`
|
||||
<mwc-button
|
||||
.label=${this.hass!.localize("ui.common.close")}
|
||||
@click=${this.closeDialog}
|
||||
></mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-textfield>
|
||||
${this._narrow
|
||||
? html`
|
||||
<mwc-button
|
||||
.label=${this.hass!.localize("ui.common.close")}
|
||||
@click=${this.closeDialog}
|
||||
></mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
${!items
|
||||
? html`<ha-circular-progress
|
||||
@@ -210,10 +215,12 @@ export class QuickBar extends LitElement {
|
||||
@keydown=${this._handleListItemKeyDown}
|
||||
@selected=${this._handleSelected}
|
||||
style=${styleMap({
|
||||
height: `${Math.min(
|
||||
items.length * (this._commandMode ? 56 : 72) + 26,
|
||||
this._done ? 500 : 0
|
||||
)}px`,
|
||||
height: this._narrow
|
||||
? "calc(100vh - 56px)"
|
||||
: `${Math.min(
|
||||
items.length * (this._commandMode ? 56 : 72) + 26,
|
||||
this._done ? 500 : 0
|
||||
)}px`,
|
||||
})}
|
||||
>
|
||||
${scroll({
|
||||
@@ -224,9 +231,7 @@ export class QuickBar extends LitElement {
|
||||
})}
|
||||
</mwc-list>
|
||||
`}
|
||||
${!this._narrow && this._hint
|
||||
? html`<div class="hint">${this._hint}</div>`
|
||||
: ""}
|
||||
${this._hint ? html`<div class="hint">${this._hint}</div>` : ""}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
@@ -705,6 +710,18 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-textfield {
|
||||
--mdc-shape-small: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 690px) {
|
||||
.hint {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
ha-icon.entity,
|
||||
ha-svg-icon.entity {
|
||||
margin-left: 20px;
|
||||
@@ -758,6 +775,11 @@ export class QuickBar extends LitElement {
|
||||
padding: 16px 0px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div[slot="trailingIcon"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -16,5 +16,6 @@ export const showQuickBar = (
|
||||
dialogTag: "ha-quick-bar",
|
||||
dialogImport: loadQuickBar,
|
||||
dialogParams,
|
||||
addHistory: false,
|
||||
});
|
||||
};
|
||||
|
@@ -5,5 +5,3 @@ import "../resources/roboto";
|
||||
import "../util/legacy-support";
|
||||
|
||||
setPassiveTouchGestures(true);
|
||||
|
||||
(window as any).frontendVersion = __VERSION__;
|
||||
|
@@ -29,6 +29,7 @@ import { HomeAssistant } from "../types";
|
||||
import { MAIN_WINDOW_NAME } from "../data/main_window";
|
||||
|
||||
window.name = MAIN_WINDOW_NAME;
|
||||
(window as any).frontendVersion = __VERSION__;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<meta name='viewport' content='width=device-width, user-scalable=no, viewport-fit=cover'>
|
||||
<meta name='viewport' content='width=device-width, user-scalable=no, viewport-fit=cover, initial-scale=1'>
|
||||
<style>
|
||||
body {
|
||||
font-family: Roboto, sans-serif;
|
||||
|
@@ -1,13 +1,17 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiImagePlus, mdiPencil } from "@mdi/js";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import { mdiImagePlus, mdiPencil } from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket/dist/types";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import { groupBy } from "../../../common/util/group-by";
|
||||
import { afterNextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
@@ -17,14 +21,19 @@ import {
|
||||
deleteAreaRegistryEntry,
|
||||
updateAreaRegistryEntry,
|
||||
} from "../../../data/area_registry";
|
||||
import { AutomationEntity } from "../../../data/automation";
|
||||
import {
|
||||
computeDeviceName,
|
||||
DeviceRegistryEntry,
|
||||
sortDeviceRegistryByName,
|
||||
} from "../../../data/device_registry";
|
||||
import {
|
||||
computeEntityRegistryName,
|
||||
EntityRegistryEntry,
|
||||
sortEntityRegistryByName,
|
||||
} from "../../../data/entity_registry";
|
||||
import { SceneEntity } from "../../../data/scene";
|
||||
import { ScriptEntity } from "../../../data/script";
|
||||
import { findRelated, RelatedResult } from "../../../data/search";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
@@ -35,11 +44,11 @@ import {
|
||||
loadAreaRegistryDetailDialog,
|
||||
showAreaRegistryDetailDialog,
|
||||
} from "./show-dialog-area-registry-detail";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { SceneEntity } from "../../../data/scene";
|
||||
import { ScriptEntity } from "../../../data/script";
|
||||
import { AutomationEntity } from "../../../data/automation";
|
||||
import { groupBy } from "../../../common/util/group-by";
|
||||
|
||||
declare type NameAndEntity<EntityType extends HassEntity> = {
|
||||
name: string;
|
||||
entity: EntityType;
|
||||
};
|
||||
|
||||
@customElement("ha-config-area-page")
|
||||
class HaConfigAreaPage extends LitElement {
|
||||
@@ -136,10 +145,59 @@ class HaConfigAreaPage extends LitElement {
|
||||
this.entities
|
||||
);
|
||||
|
||||
const grouped = groupBy(entities, (entity) =>
|
||||
// Pre-compute the entity and device names, so we can sort by them
|
||||
if (devices) {
|
||||
devices.forEach((entry) => {
|
||||
entry.name = computeDeviceName(entry, this.hass);
|
||||
});
|
||||
sortDeviceRegistryByName(devices);
|
||||
}
|
||||
if (entities) {
|
||||
entities.forEach((entry) => {
|
||||
entry.name = computeEntityRegistryName(this.hass, entry);
|
||||
});
|
||||
sortEntityRegistryByName(entities);
|
||||
}
|
||||
|
||||
// Group entities by domain
|
||||
const groupedEntities = groupBy(entities, (entity) =>
|
||||
computeDomain(entity.entity_id)
|
||||
);
|
||||
|
||||
// Pre-compute the name also for the grouped and related entities so we can sort by them
|
||||
let groupedAutomations: NameAndEntity<AutomationEntity>[] = [];
|
||||
let groupedScenes: NameAndEntity<SceneEntity>[] = [];
|
||||
let groupedScripts: NameAndEntity<ScriptEntity>[] = [];
|
||||
let relatedAutomations: NameAndEntity<AutomationEntity>[] = [];
|
||||
let relatedScenes: NameAndEntity<SceneEntity>[] = [];
|
||||
let relatedScripts: NameAndEntity<ScriptEntity>[] = [];
|
||||
|
||||
if (isComponentLoaded(this.hass, "automation")) {
|
||||
({
|
||||
groupedEntities: groupedAutomations,
|
||||
relatedEntities: relatedAutomations,
|
||||
} = this._prepareEntities<AutomationEntity>(
|
||||
groupedEntities.automation,
|
||||
this._related?.automation
|
||||
));
|
||||
}
|
||||
|
||||
if (isComponentLoaded(this.hass, "scene")) {
|
||||
({ groupedEntities: groupedScenes, relatedEntities: relatedScenes } =
|
||||
this._prepareEntities<SceneEntity>(
|
||||
groupedEntities.scene,
|
||||
this._related?.scene
|
||||
));
|
||||
}
|
||||
|
||||
if (isComponentLoaded(this.hass, "script")) {
|
||||
({ groupedEntities: groupedScripts, relatedEntities: relatedScripts } =
|
||||
this._prepareEntities<ScriptEntity>(
|
||||
groupedEntities.script,
|
||||
this._related?.script
|
||||
));
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
@@ -208,9 +266,7 @@ class HaConfigAreaPage extends LitElement {
|
||||
html`
|
||||
<a href="/config/devices/device/${device.id}">
|
||||
<paper-item>
|
||||
<paper-item-body>
|
||||
${computeDeviceName(device, this.hass)}
|
||||
</paper-item-body>
|
||||
<paper-item-body> ${device.name} </paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
@@ -240,9 +296,7 @@ class HaConfigAreaPage extends LitElement {
|
||||
@click=${this._openEntity}
|
||||
.entity=${entity}
|
||||
>
|
||||
<paper-item-body>
|
||||
${computeEntityRegistryName(this.hass, entity)}
|
||||
</paper-item-body>
|
||||
<paper-item-body> ${entity.name} </paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
`
|
||||
@@ -261,46 +315,36 @@ class HaConfigAreaPage extends LitElement {
|
||||
? html`
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.automations"
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
)}
|
||||
>
|
||||
${grouped.automation?.length
|
||||
${groupedAutomations?.length
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.assigned_to_area"
|
||||
)}:
|
||||
</h3>
|
||||
${grouped.automation.map((entity) => {
|
||||
const entityState = this.hass.states[
|
||||
entity.entity_id
|
||||
] as AutomationEntity | undefined;
|
||||
return entityState
|
||||
? this._renderAutomation(entityState)
|
||||
: "";
|
||||
})}`
|
||||
${groupedAutomations.map((automation) =>
|
||||
this._renderAutomation(
|
||||
automation.name,
|
||||
automation.entity
|
||||
)
|
||||
)}`
|
||||
: ""}
|
||||
${this._related?.automation?.filter(
|
||||
(entityId) =>
|
||||
!grouped.automation?.find(
|
||||
(entity) => entity.entity_id === entityId
|
||||
)
|
||||
).length
|
||||
${relatedAutomations?.length
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.targeting_area"
|
||||
)}:
|
||||
</h3>
|
||||
${this._related.automation.map((scene) => {
|
||||
const entityState = this.hass.states[scene] as
|
||||
| AutomationEntity
|
||||
| undefined;
|
||||
return entityState
|
||||
? this._renderAutomation(entityState)
|
||||
: "";
|
||||
})}`
|
||||
${relatedAutomations.map((automation) =>
|
||||
this._renderAutomation(
|
||||
automation.name,
|
||||
automation.entity
|
||||
)
|
||||
)}`
|
||||
: ""}
|
||||
${!grouped.automation?.length &&
|
||||
!this._related?.automation?.length
|
||||
${!groupedAutomations?.length && !relatedAutomations?.length
|
||||
? html`
|
||||
<paper-item class="no-link"
|
||||
>${this.hass.localize(
|
||||
@@ -318,42 +362,30 @@ class HaConfigAreaPage extends LitElement {
|
||||
? html`
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.scenes"
|
||||
"ui.panel.config.devices.scene.scenes_heading"
|
||||
)}
|
||||
>
|
||||
${grouped.scene?.length
|
||||
${groupedScenes?.length
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.assigned_to_area"
|
||||
)}:
|
||||
</h3>
|
||||
${grouped.scene.map((entity) => {
|
||||
const entityState =
|
||||
this.hass.states[entity.entity_id];
|
||||
return entityState
|
||||
? this._renderScene(entityState)
|
||||
: "";
|
||||
})}`
|
||||
${groupedScenes.map((scene) =>
|
||||
this._renderScene(scene.name, scene.entity)
|
||||
)}`
|
||||
: ""}
|
||||
${this._related?.scene?.filter(
|
||||
(entityId) =>
|
||||
!grouped.scene?.find(
|
||||
(entity) => entity.entity_id === entityId
|
||||
)
|
||||
).length
|
||||
${relatedScenes?.length
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.targeting_area"
|
||||
)}:
|
||||
</h3>
|
||||
${this._related.scene.map((scene) => {
|
||||
const entityState = this.hass.states[scene];
|
||||
return entityState
|
||||
? this._renderScene(entityState)
|
||||
: "";
|
||||
})}`
|
||||
${relatedScenes.map((scene) =>
|
||||
this._renderScene(scene.name, scene.entity)
|
||||
)}`
|
||||
: ""}
|
||||
${!grouped.scene?.length && !this._related?.scene?.length
|
||||
${!groupedScenes?.length && !relatedScenes?.length
|
||||
? html`
|
||||
<paper-item class="no-link"
|
||||
>${this.hass.localize(
|
||||
@@ -369,45 +401,30 @@ class HaConfigAreaPage extends LitElement {
|
||||
? html`
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.script.scripts"
|
||||
"ui.panel.config.devices.script.scripts_heading"
|
||||
)}
|
||||
>
|
||||
${grouped.script?.length
|
||||
${groupedScripts?.length
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.assigned_to_area"
|
||||
)}:
|
||||
</h3>
|
||||
${grouped.script.map((entity) => {
|
||||
const entityState = this.hass.states[
|
||||
entity.entity_id
|
||||
] as ScriptEntity | undefined;
|
||||
return entityState
|
||||
? this._renderScript(entityState)
|
||||
: "";
|
||||
})}`
|
||||
${groupedScripts.map((script) =>
|
||||
this._renderScript(script.name, script.entity)
|
||||
)}`
|
||||
: ""}
|
||||
${this._related?.script?.filter(
|
||||
(entityId) =>
|
||||
!grouped.script?.find(
|
||||
(entity) => entity.entity_id === entityId
|
||||
)
|
||||
).length
|
||||
${relatedScripts?.length
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.targeting_area"
|
||||
)}:
|
||||
</h3>
|
||||
${this._related.script.map((scene) => {
|
||||
const entityState = this.hass.states[scene] as
|
||||
| ScriptEntity
|
||||
| undefined;
|
||||
return entityState
|
||||
? this._renderScript(entityState)
|
||||
: "";
|
||||
})}`
|
||||
${relatedScripts.map((script) =>
|
||||
this._renderScript(script.name, script.entity)
|
||||
)}`
|
||||
: ""}
|
||||
${!grouped.script?.length && !this._related?.script?.length
|
||||
${!groupedScripts?.length && !relatedScripts?.length
|
||||
? html`
|
||||
<paper-item class="no-link"
|
||||
>${this.hass.localize(
|
||||
@@ -425,7 +442,51 @@ class HaConfigAreaPage extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderScene(entityState: SceneEntity) {
|
||||
private _prepareEntities<EntityType extends HassEntity>(
|
||||
entries?: EntityRegistryEntry[],
|
||||
relatedEntityIds?: string[]
|
||||
): {
|
||||
groupedEntities: NameAndEntity<EntityType>[];
|
||||
relatedEntities: NameAndEntity<EntityType>[];
|
||||
} {
|
||||
const groupedEntities: NameAndEntity<EntityType>[] = [];
|
||||
const relatedEntities: NameAndEntity<EntityType>[] = [];
|
||||
|
||||
if (entries?.length) {
|
||||
entries.forEach((entity) => {
|
||||
const entityState = this.hass.states[
|
||||
entity.entity_id
|
||||
] as unknown as EntityType;
|
||||
if (entityState) {
|
||||
groupedEntities.push({
|
||||
name: computeStateName(entityState),
|
||||
entity: entityState,
|
||||
});
|
||||
}
|
||||
});
|
||||
groupedEntities.sort((entry1, entry2) =>
|
||||
caseInsensitiveStringCompare(entry1.name!, entry2.name!)
|
||||
);
|
||||
}
|
||||
if (relatedEntityIds?.length) {
|
||||
relatedEntityIds.forEach((entity) => {
|
||||
const entityState = this.hass.states[entity] as EntityType;
|
||||
if (entityState) {
|
||||
relatedEntities.push({
|
||||
name: entityState ? computeStateName(entityState) : "",
|
||||
entity: entityState,
|
||||
});
|
||||
}
|
||||
});
|
||||
relatedEntities.sort((entry1, entry2) =>
|
||||
caseInsensitiveStringCompare(entry1.name!, entry2.name!)
|
||||
);
|
||||
}
|
||||
|
||||
return { groupedEntities, relatedEntities };
|
||||
}
|
||||
|
||||
private _renderScene(name: string, entityState: SceneEntity) {
|
||||
return html`<div>
|
||||
<a
|
||||
href=${ifDefined(
|
||||
@@ -435,7 +496,7 @@ class HaConfigAreaPage extends LitElement {
|
||||
)}
|
||||
>
|
||||
<paper-item .disabled=${!entityState.attributes.id}>
|
||||
<paper-item-body> ${computeStateName(entityState)} </paper-item-body>
|
||||
<paper-item-body> ${name} </paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
@@ -449,7 +510,7 @@ class HaConfigAreaPage extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderAutomation(entityState: AutomationEntity) {
|
||||
private _renderAutomation(name: string, entityState: AutomationEntity) {
|
||||
return html`<div>
|
||||
<a
|
||||
href=${ifDefined(
|
||||
@@ -459,7 +520,7 @@ class HaConfigAreaPage extends LitElement {
|
||||
)}
|
||||
>
|
||||
<paper-item .disabled=${!entityState.attributes.id}>
|
||||
<paper-item-body> ${computeStateName(entityState)} </paper-item-body>
|
||||
<paper-item-body> ${name} </paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
@@ -473,10 +534,10 @@ class HaConfigAreaPage extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderScript(entityState: ScriptEntity) {
|
||||
private _renderScript(name: string, entityState: ScriptEntity) {
|
||||
return html`<a href=${`/config/script/edit/${entityState.entity_id}`}>
|
||||
<paper-item>
|
||||
<paper-item-body> ${computeStateName(entityState)} </paper-item-body>
|
||||
<paper-item-body> ${name} </paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>`;
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import "@material/mwc-button";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import { LitElement, css, html, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { formatDateTime } from "../../../../common/datetime/format_date_time";
|
||||
@@ -7,6 +10,8 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
|
||||
import "../../../../components/buttons/ha-call-api-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import {
|
||||
cloudLogout,
|
||||
CloudStatusLoggedIn,
|
||||
@@ -21,9 +26,10 @@ import "./cloud-google-pref";
|
||||
import "./cloud-remote-pref";
|
||||
import "./cloud-tts-pref";
|
||||
import "./cloud-webhooks";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
|
||||
@customElement("cloud-account")
|
||||
export class CloudAccount extends LitElement {
|
||||
export class CloudAccount extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public isWide = false;
|
||||
@@ -43,6 +49,23 @@ export class CloudAccount extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
header="Home Assistant Cloud"
|
||||
>
|
||||
<ha-button-menu
|
||||
slot="toolbar-icon"
|
||||
corner="BOTTOM_START"
|
||||
@action=${this._handleMenuAction}
|
||||
activatable
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<mwc-list-item>
|
||||
${this.hass.localize("ui.panel.config.cloud.account.sign_out")}
|
||||
</mwc-list-item>
|
||||
</ha-button-menu>
|
||||
|
||||
<div class="content">
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<span slot="header">Home Assistant Cloud</span>
|
||||
@@ -115,11 +138,6 @@ export class CloudAccount extends LitElement {
|
||||
)}
|
||||
</mwc-button>
|
||||
</a>
|
||||
<mwc-button @click=${this._handleLogout}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.sign_out"
|
||||
)}</mwc-button
|
||||
>
|
||||
</div>
|
||||
</ha-card>
|
||||
</ha-config-section>
|
||||
@@ -200,6 +218,33 @@ export class CloudAccount extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected override hassSubscribe() {
|
||||
const googleCheck = () => {
|
||||
if (!this.cloudStatus?.google_registered) {
|
||||
fireEvent(this, "ha-refresh-cloud-status");
|
||||
}
|
||||
};
|
||||
return [
|
||||
this.hass.connection.subscribeEvents(() => {
|
||||
if (!this.cloudStatus?.alexa_registered) {
|
||||
fireEvent(this, "ha-refresh-cloud-status");
|
||||
}
|
||||
}, "alexa_smart_home"),
|
||||
this.hass.connection.subscribeEvents(
|
||||
googleCheck,
|
||||
"google_assistant_command"
|
||||
),
|
||||
this.hass.connection.subscribeEvents(
|
||||
googleCheck,
|
||||
"google_assistant_query"
|
||||
),
|
||||
this.hass.connection.subscribeEvents(
|
||||
googleCheck,
|
||||
"google_assistant_sync"
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private async _fetchSubscriptionInfo() {
|
||||
this._subscription = await fetchCloudSubscriptionInfo(this.hass);
|
||||
if (
|
||||
@@ -211,9 +256,12 @@ export class CloudAccount extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleLogout() {
|
||||
await cloudLogout(this.hass);
|
||||
fireEvent(this, "ha-refresh-cloud-status");
|
||||
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
await cloudLogout(this.hass);
|
||||
fireEvent(this, "ha-refresh-cloud-status");
|
||||
}
|
||||
}
|
||||
|
||||
_computeRTLDirection(hass) {
|
||||
@@ -237,7 +285,7 @@ export class CloudAccount extends LitElement {
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.card-actions a {
|
||||
text-decoration: none;
|
||||
|
@@ -10,7 +10,7 @@ import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
export class CloudAlexaPref extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public cloudStatus?: CloudStatusLoggedIn;
|
||||
|
||||
@@ -21,6 +21,7 @@ export class CloudAlexaPref extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const alexa_registered = this.cloudStatus.alexa_registered;
|
||||
const { alexa_enabled, alexa_report_state } = this.cloudStatus!.prefs;
|
||||
|
||||
return html`
|
||||
@@ -36,33 +37,49 @@ export class CloudAlexaPref extends LitElement {
|
||||
></ha-switch>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
${this.hass!.localize("ui.panel.config.cloud.account.alexa.info")}
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="https://skills-store.amazon.com/deeplink/dp/B0772J1QKB?deviceType=app"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.cloud.account.alexa.enable_ha_skill"
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.nabucasa.com/config/amazon_alexa/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.cloud.account.alexa.config_documentation"
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
${alexa_enabled
|
||||
<p>
|
||||
${this.hass!.localize("ui.panel.config.cloud.account.alexa.info")}
|
||||
</p>
|
||||
${!alexa_enabled
|
||||
? ""
|
||||
: !alexa_registered
|
||||
? html`
|
||||
<ha-alert
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.alexa.not_configured_title"
|
||||
)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.alexa.not_configured_text"
|
||||
)}
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="https://skills-store.amazon.com/deeplink/dp/B0772J1QKB?deviceType=app"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.cloud.account.alexa.enable_ha_skill"
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.nabucasa.com/config/amazon_alexa/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.cloud.account.alexa.config_documentation"
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</ha-alert>
|
||||
`
|
||||
: html`
|
||||
<div class="state-reporting">
|
||||
<h3>
|
||||
${this.hass!.localize(
|
||||
@@ -81,18 +98,21 @@ export class CloudAlexaPref extends LitElement {
|
||||
"ui.panel.config.cloud.account.alexa.info_state_reporting"
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
`}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button
|
||||
@click=${this._handleSync}
|
||||
.disabled=${!alexa_enabled || this._syncing}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.cloud.account.alexa.sync_entities"
|
||||
)}
|
||||
</mwc-button>
|
||||
${alexa_registered
|
||||
? html`
|
||||
<mwc-button
|
||||
@click=${this._handleSync}
|
||||
.disabled=${!alexa_enabled || this._syncing}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.cloud.account.alexa.sync_entities"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
<div class="spacer"></div>
|
||||
<a href="/config/cloud/alexa">
|
||||
<mwc-button
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import "@material/mwc-textfield/mwc-textfield";
|
||||
import type { TextField } from "@material/mwc-textfield/mwc-textfield";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { property } from "lit/decorators";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/buttons/ha-call-api-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-card";
|
||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
||||
import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud";
|
||||
import { syncCloudGoogleEntities } from "../../../../data/google_assistant";
|
||||
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
@@ -16,13 +16,16 @@ import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
export class CloudGooglePref extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public cloudStatus?: CloudStatusLoggedIn;
|
||||
@property({ attribute: false }) public cloudStatus?: CloudStatusLoggedIn;
|
||||
|
||||
@state() private _syncing = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.cloudStatus) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const google_registered = this.cloudStatus.google_registered;
|
||||
const { google_enabled, google_report_state, google_secure_devices_pin } =
|
||||
this.cloudStatus.prefs;
|
||||
|
||||
@@ -43,7 +46,9 @@ export class CloudGooglePref extends LitElement {
|
||||
<p>
|
||||
${this.hass.localize("ui.panel.config.cloud.account.google.info")}
|
||||
</p>
|
||||
${google_enabled && !this.cloudStatus.google_registered
|
||||
${!google_enabled
|
||||
? ""
|
||||
: !google_registered
|
||||
? html`
|
||||
<ha-alert
|
||||
.title=${this.hass.localize(
|
||||
@@ -80,9 +85,30 @@ export class CloudGooglePref extends LitElement {
|
||||
</ul>
|
||||
</ha-alert>
|
||||
`
|
||||
: ""}
|
||||
${google_enabled
|
||||
? html`
|
||||
: html`
|
||||
${this.cloudStatus.http_use_ssl
|
||||
? html`
|
||||
<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.google.http_use_ssl_warning_title"
|
||||
)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.google.http_use_ssl_warning_text"
|
||||
)}
|
||||
<a
|
||||
href="https://www.nabucasa.com/config/google_assistant/#local-communication"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.common.learn_more"
|
||||
)}</a
|
||||
>
|
||||
</ha-alert>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<div class="state-reporting">
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
@@ -110,32 +136,34 @@ export class CloudGooglePref extends LitElement {
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.google.enter_pin_info"
|
||||
)}
|
||||
<paper-input
|
||||
label=${this.hass.localize(
|
||||
<mwc-textfield
|
||||
id="google_secure_devices_pin"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.google.devices_pin"
|
||||
)}
|
||||
id="google_secure_devices_pin"
|
||||
placeholder=${this.hass.localize(
|
||||
.placeholder=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.google.enter_pin_hint"
|
||||
)}
|
||||
.value=${google_secure_devices_pin || ""}
|
||||
@change=${this._pinChanged}
|
||||
></paper-input>
|
||||
></mwc-textfield>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button
|
||||
.hass=${this.hass}
|
||||
.disabled=${!google_enabled}
|
||||
@hass-api-called=${this._syncEntitiesCalled}
|
||||
path="cloud/google_actions/sync"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.google.sync_entities"
|
||||
)}
|
||||
</ha-call-api-button>
|
||||
${google_registered
|
||||
? html`
|
||||
<mwc-button
|
||||
@click=${this._handleSync}
|
||||
.disabled=${!google_enabled || this._syncing}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.google.sync_entities"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
<div class="spacer"></div>
|
||||
<a href="/config/cloud/google-assistant">
|
||||
<mwc-button>
|
||||
${this.hass.localize(
|
||||
@@ -148,24 +176,31 @@ export class CloudGooglePref extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _syncEntitiesCalled(ev: CustomEvent) {
|
||||
if (!ev.detail.success && ev.detail.response.status_code === 404) {
|
||||
this._syncFailed();
|
||||
private async _handleSync() {
|
||||
this._syncing = true;
|
||||
try {
|
||||
await syncCloudGoogleEntities(this.hass!);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
`ui.panel.config.cloud.account.google.${
|
||||
err.status_code === 404
|
||||
? "not_configured_title"
|
||||
: "sync_failed_title"
|
||||
}`
|
||||
),
|
||||
text: this.hass.localize(
|
||||
`ui.panel.config.cloud.account.google.${
|
||||
err.status_code === 404 ? "not_configured_text" : "sync_failed_text"
|
||||
}`
|
||||
),
|
||||
});
|
||||
fireEvent(this, "ha-refresh-cloud-status");
|
||||
} finally {
|
||||
this._syncing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _syncFailed() {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.cloud.account.google.not_configured_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.cloud.account.google.not_configured_text"
|
||||
),
|
||||
});
|
||||
fireEvent(this, "ha-refresh-cloud-status");
|
||||
}
|
||||
|
||||
private async _enableToggleChanged(ev) {
|
||||
const toggle = ev.target as HaSwitch;
|
||||
try {
|
||||
@@ -194,7 +229,7 @@ export class CloudGooglePref extends LitElement {
|
||||
}
|
||||
|
||||
private async _pinChanged(ev) {
|
||||
const input = ev.target as PaperInputElement;
|
||||
const input = ev.target as TextField;
|
||||
try {
|
||||
await updateCloudPref(this.hass, {
|
||||
[input.id]: input.value || null,
|
||||
@@ -207,7 +242,7 @@ export class CloudGooglePref extends LitElement {
|
||||
"ui.panel.config.cloud.account.google.enter_pin_error"
|
||||
)} ${err.message}`
|
||||
);
|
||||
input.value = this.cloudStatus!.prefs.google_secure_devices_pin;
|
||||
input.value = this.cloudStatus!.prefs.google_secure_devices_pin || "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,16 +260,13 @@ export class CloudGooglePref extends LitElement {
|
||||
right: auto;
|
||||
left: 24px;
|
||||
}
|
||||
ha-call-api-button {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
paper-input {
|
||||
mwc-textfield {
|
||||
width: 250px;
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.card-actions a {
|
||||
text-decoration: none;
|
||||
@@ -245,6 +277,10 @@ export class CloudGooglePref extends LitElement {
|
||||
.secure_devices {
|
||||
padding-top: 8px;
|
||||
}
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.state-reporting {
|
||||
display: flex;
|
||||
margin-top: 1.5em;
|
||||
|
@@ -6,6 +6,7 @@ import {
|
||||
mdiCloseBox,
|
||||
mdiCloseBoxMultiple,
|
||||
} from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
@@ -33,9 +34,14 @@ import {
|
||||
updateCloudAlexaEntityConfig,
|
||||
updateCloudPref,
|
||||
} from "../../../../data/cloud";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../../data/entity_registry";
|
||||
import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler";
|
||||
import "../../../../layouts/hass-loading-screen";
|
||||
import "../../../../layouts/hass-subpage";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
@@ -43,7 +49,7 @@ const DEFAULT_CONFIG_EXPOSE = true;
|
||||
const IGNORE_INTERFACES = ["Alexa.EndpointHealth"];
|
||||
|
||||
@customElement("cloud-alexa")
|
||||
class CloudAlexa extends LitElement {
|
||||
class CloudAlexa extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property()
|
||||
@@ -53,9 +59,15 @@ class CloudAlexa extends LitElement {
|
||||
|
||||
@state() private _entities?: AlexaEntity[];
|
||||
|
||||
@property()
|
||||
@state()
|
||||
private _entityConfigs: CloudPreferences["alexa_entity_configs"] = {};
|
||||
|
||||
@state()
|
||||
private _entityCategories?: Record<
|
||||
string,
|
||||
EntityRegistryEntry["entity_category"]
|
||||
>;
|
||||
|
||||
private _popstateSyncAttached = false;
|
||||
|
||||
private _popstateReloadStatusAttached = false;
|
||||
@@ -72,7 +84,7 @@ class CloudAlexa extends LitElement {
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this._entities === undefined) {
|
||||
if (this._entities === undefined || this._entityCategories === undefined) {
|
||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
||||
}
|
||||
const emptyFilter = isEmptyFilter(this.cloudStatus.alexa_entities);
|
||||
@@ -99,10 +111,17 @@ class CloudAlexa extends LitElement {
|
||||
should_expose: null,
|
||||
};
|
||||
const isExposed = emptyFilter
|
||||
? this._configIsExposed(entity.entity_id, config)
|
||||
? this._configIsExposed(
|
||||
entity.entity_id,
|
||||
config,
|
||||
this._entityCategories![entity.entity_id]
|
||||
)
|
||||
: filterFunc(entity.entity_id);
|
||||
const isDomainExposed = emptyFilter
|
||||
? this._configIsDomainExposed(entity.entity_id)
|
||||
? this._configIsDomainExposed(
|
||||
entity.entity_id,
|
||||
this._entityCategories![entity.entity_id]
|
||||
)
|
||||
: filterFunc(entity.entity_id);
|
||||
if (isExposed) {
|
||||
selected++;
|
||||
@@ -287,6 +306,23 @@ class CloudAlexa extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected override hassSubscribe(): (
|
||||
| UnsubscribeFunc
|
||||
| Promise<UnsubscribeFunc>
|
||||
)[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection, (entries) => {
|
||||
const categories = {};
|
||||
|
||||
for (const entry of entries) {
|
||||
categories[entry.entity_id] = entry.entity_category;
|
||||
}
|
||||
|
||||
this._entityCategories = categories;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
const entities = await fetchCloudAlexaEntities(this.hass);
|
||||
entities.sort((a, b) => {
|
||||
@@ -305,15 +341,26 @@ class CloudAlexa extends LitElement {
|
||||
fireEvent(this, "hass-more-info", { entityId });
|
||||
}
|
||||
|
||||
private _configIsDomainExposed(entityId: string) {
|
||||
private _configIsDomainExposed(
|
||||
entityId: string,
|
||||
entityCategory: EntityRegistryEntry["entity_category"] | undefined
|
||||
) {
|
||||
const domain = computeDomain(entityId);
|
||||
return this.cloudStatus.prefs.alexa_default_expose
|
||||
? this.cloudStatus.prefs.alexa_default_expose.includes(domain)
|
||||
? !entityCategory &&
|
||||
this.cloudStatus.prefs.alexa_default_expose.includes(domain)
|
||||
: DEFAULT_CONFIG_EXPOSE;
|
||||
}
|
||||
|
||||
private _configIsExposed(entityId: string, config: AlexaEntityConfig) {
|
||||
return config.should_expose ?? this._configIsDomainExposed(entityId);
|
||||
private _configIsExposed(
|
||||
entityId: string,
|
||||
config: AlexaEntityConfig,
|
||||
entityCategory: EntityRegistryEntry["entity_category"] | undefined
|
||||
) {
|
||||
return (
|
||||
config.should_expose ??
|
||||
this._configIsDomainExposed(entityId, entityCategory)
|
||||
);
|
||||
}
|
||||
|
||||
private async _exposeChanged(ev: CustomEvent<ActionDetail>) {
|
||||
|
@@ -6,6 +6,7 @@ import {
|
||||
mdiCloseBox,
|
||||
mdiCloseBoxMultiple,
|
||||
} from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
@@ -35,6 +36,10 @@ import {
|
||||
updateCloudGoogleEntityConfig,
|
||||
updateCloudPref,
|
||||
} from "../../../../data/cloud";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../../data/entity_registry";
|
||||
import {
|
||||
fetchCloudGoogleEntities,
|
||||
GoogleEntity,
|
||||
@@ -42,6 +47,7 @@ import {
|
||||
import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler";
|
||||
import "../../../../layouts/hass-loading-screen";
|
||||
import "../../../../layouts/hass-subpage";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
@@ -49,7 +55,7 @@ import { showToast } from "../../../../util/toast";
|
||||
const DEFAULT_CONFIG_EXPOSE = true;
|
||||
|
||||
@customElement("cloud-google-assistant")
|
||||
class CloudGoogleAssistant extends LitElement {
|
||||
class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public cloudStatus!: CloudStatusLoggedIn;
|
||||
@@ -58,9 +64,15 @@ class CloudGoogleAssistant extends LitElement {
|
||||
|
||||
@state() private _entities?: GoogleEntity[];
|
||||
|
||||
@property()
|
||||
@state()
|
||||
private _entityConfigs: CloudPreferences["google_entity_configs"] = {};
|
||||
|
||||
@state()
|
||||
private _entityCategories?: Record<
|
||||
string,
|
||||
EntityRegistryEntry["entity_category"]
|
||||
>;
|
||||
|
||||
private _popstateSyncAttached = false;
|
||||
|
||||
private _popstateReloadStatusAttached = false;
|
||||
@@ -77,7 +89,7 @@ class CloudGoogleAssistant extends LitElement {
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this._entities === undefined) {
|
||||
if (this._entities === undefined || this._entityCategories === undefined) {
|
||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
||||
}
|
||||
const emptyFilter = isEmptyFilter(this.cloudStatus.google_entities);
|
||||
@@ -105,10 +117,17 @@ class CloudGoogleAssistant extends LitElement {
|
||||
should_expose: null,
|
||||
};
|
||||
const isExposed = emptyFilter
|
||||
? this._configIsExposed(entity.entity_id, config)
|
||||
? this._configIsExposed(
|
||||
entity.entity_id,
|
||||
config,
|
||||
this._entityCategories![entity.entity_id]
|
||||
)
|
||||
: filterFunc(entity.entity_id);
|
||||
const isDomainExposed = emptyFilter
|
||||
? this._configIsDomainExposed(entity.entity_id)
|
||||
? this._configIsDomainExposed(
|
||||
entity.entity_id,
|
||||
this._entityCategories![entity.entity_id]
|
||||
)
|
||||
: filterFunc(entity.entity_id);
|
||||
if (isExposed) {
|
||||
selected++;
|
||||
@@ -311,15 +330,43 @@ class CloudGoogleAssistant extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _configIsDomainExposed(entityId: string) {
|
||||
protected override hassSubscribe(): (
|
||||
| UnsubscribeFunc
|
||||
| Promise<UnsubscribeFunc>
|
||||
)[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection, (entries) => {
|
||||
const categories = {};
|
||||
|
||||
for (const entry of entries) {
|
||||
categories[entry.entity_id] = entry.entity_category;
|
||||
}
|
||||
|
||||
this._entityCategories = categories;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
private _configIsDomainExposed(
|
||||
entityId: string,
|
||||
entityCategory: EntityRegistryEntry["entity_category"] | undefined
|
||||
) {
|
||||
const domain = computeDomain(entityId);
|
||||
return this.cloudStatus.prefs.google_default_expose
|
||||
? this.cloudStatus.prefs.google_default_expose.includes(domain)
|
||||
? !entityCategory &&
|
||||
this.cloudStatus.prefs.google_default_expose.includes(domain)
|
||||
: DEFAULT_CONFIG_EXPOSE;
|
||||
}
|
||||
|
||||
private _configIsExposed(entityId: string, config: GoogleEntityConfig) {
|
||||
return config.should_expose ?? this._configIsDomainExposed(entityId);
|
||||
private _configIsExposed(
|
||||
entityId: string,
|
||||
config: GoogleEntityConfig,
|
||||
entityCategory: EntityRegistryEntry["entity_category"] | undefined
|
||||
) {
|
||||
return (
|
||||
config.should_expose ??
|
||||
this._configIsDomainExposed(entityId, entityCategory)
|
||||
);
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
|
@@ -3,7 +3,14 @@ import "@material/mwc-list/mwc-list-item";
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/ha-card";
|
||||
@@ -26,6 +33,7 @@ import "./ha-config-navigation";
|
||||
import "./ha-config-updates";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showToast } from "../../../util/toast";
|
||||
|
||||
@customElement("ha-config-dashboard")
|
||||
class HaConfigDashboard extends LitElement {
|
||||
@@ -43,6 +51,8 @@ class HaConfigDashboard extends LitElement {
|
||||
|
||||
@property() public showAdvanced!: boolean;
|
||||
|
||||
private _notifyUpdates = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-app-layout>
|
||||
@@ -129,6 +139,26 @@ class HaConfigDashboard extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected override updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (!changedProps.has("supervisorUpdates") || !this._notifyUpdates) {
|
||||
return;
|
||||
}
|
||||
this._notifyUpdates = false;
|
||||
if (this.supervisorUpdates?.length) {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.updates.updates_refreshed"
|
||||
),
|
||||
});
|
||||
} else {
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.panel.config.updates.no_new_updates"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _showQuickBar(): void {
|
||||
showQuickBar(this, {
|
||||
commandMode: true,
|
||||
@@ -140,6 +170,7 @@ class HaConfigDashboard extends LitElement {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
this._notifyUpdates = true;
|
||||
await refreshSupervisorAvailableUpdates(this.hass);
|
||||
fireEvent(this, "ha-refresh-supervisor");
|
||||
return;
|
||||
@@ -161,10 +192,6 @@ class HaConfigDashboard extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
app-header {
|
||||
border-bottom: var(--app-header-border-bottom);
|
||||
--header-height: 55px;
|
||||
}
|
||||
:host(:not([narrow])) ha-card:last-child {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
@@ -8,7 +8,6 @@ import "../../../components/ha-alert";
|
||||
import "../../../components/ha-logo-svg";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { SupervisorAvailableUpdates } from "../../../data/supervisor/root";
|
||||
import { buttonLinkStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-icon-next";
|
||||
|
||||
@@ -78,7 +77,7 @@ class HaConfigUpdates extends LitElement {
|
||||
)}
|
||||
${!this._showAll && this.supervisorUpdates.length >= 4
|
||||
? html`
|
||||
<button class="link show-all" @click=${this._showAllClicked}>
|
||||
<button class="show-more" @click=${this._showAllClicked}>
|
||||
${this.hass.localize("ui.panel.config.updates.more_updates", {
|
||||
count: this.supervisorUpdates!.length - updates.length,
|
||||
})}
|
||||
@@ -94,7 +93,6 @@ class HaConfigUpdates extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup[] {
|
||||
return [
|
||||
buttonLinkStyle,
|
||||
css`
|
||||
.title {
|
||||
font-size: 16px;
|
||||
@@ -125,6 +123,22 @@ class HaConfigUpdates extends LitElement {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
button.show-more {
|
||||
color: var(--primary-color);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border-width: initial;
|
||||
border-style: none;
|
||||
border-color: initial;
|
||||
border-image: initial;
|
||||
padding: 16px;
|
||||
font: inherit;
|
||||
}
|
||||
button.show-more:focus {
|
||||
outline: none;
|
||||
text-decoration: underline;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -23,7 +23,15 @@ export class HaDeviceCard extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card
|
||||
.header=${this.hass.localize("ui.panel.config.devices.device_info")}
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.device_info",
|
||||
"type",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.type.${
|
||||
this.device.entry_type || "device"
|
||||
}_heading`
|
||||
)
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.device.model
|
||||
@@ -59,7 +67,12 @@ export class HaDeviceCard extends LitElement {
|
||||
? html`
|
||||
<div class="extra-info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.firmware",
|
||||
`ui.panel.config.integrations.config_entry.${
|
||||
this.device.entry_type === "service" &&
|
||||
!this.device.hw_version
|
||||
? "version"
|
||||
: "firmware"
|
||||
}`,
|
||||
"version",
|
||||
this.device.sw_version
|
||||
)}
|
||||
|
@@ -82,12 +82,26 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
</ha-switch>
|
||||
<div>
|
||||
<div>
|
||||
${this.hass.localize("ui.panel.config.devices.enabled_label")}
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.enabled_label",
|
||||
"type",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.type.${
|
||||
device.entry_type || "device"
|
||||
}`
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div class="secondary">
|
||||
${this._disabledBy && this._disabledBy !== "user"
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.devices.enabled_cause",
|
||||
"type",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.type.${
|
||||
device.entry_type || "device"
|
||||
}`
|
||||
),
|
||||
"cause",
|
||||
this.hass.localize(
|
||||
`config_entry.disabled_by.${this._disabledBy}`
|
||||
|
@@ -39,6 +39,7 @@ import {
|
||||
findBatteryEntity,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../../data/entity_registry";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import { SceneEntities, showSceneEditor } from "../../../data/scene";
|
||||
import { findRelated, RelatedResult } from "../../../data/search";
|
||||
import {
|
||||
@@ -201,6 +202,10 @@ export class HaConfigDevicePage extends LitElement {
|
||||
}
|
||||
|
||||
private async _renderDiagnosticButtons(requestId: number): Promise<void> {
|
||||
if (!isComponentLoaded(this.hass, "diagnostics")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const device = this._device(this.deviceId, this.devices);
|
||||
|
||||
if (!device) {
|
||||
@@ -208,34 +213,53 @@ export class HaConfigDevicePage extends LitElement {
|
||||
}
|
||||
|
||||
let links = await Promise.all(
|
||||
this._integrations(device, this.entries)
|
||||
.filter((entry) => entry.state === "loaded")
|
||||
.map(async (entry) => {
|
||||
const info = await fetchDiagnosticHandler(this.hass, entry.domain);
|
||||
this._integrations(device, this.entries).map(async (entry) => {
|
||||
if (entry.state !== "loaded") {
|
||||
return false;
|
||||
}
|
||||
const info = await fetchDiagnosticHandler(this.hass, entry.domain);
|
||||
|
||||
if (!info.handlers.device && !info.handlers.config_entry) {
|
||||
return "";
|
||||
}
|
||||
const link = info.handlers.device
|
||||
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);
|
||||
return html`
|
||||
<a href=${link} @click=${this._signUrl}>
|
||||
<mwc-button>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.devices.download_diagnostics`
|
||||
)}
|
||||
</mwc-button>
|
||||
</a>
|
||||
`;
|
||||
})
|
||||
: getConfigEntryDiagnosticsDownloadUrl(entry.entry_id),
|
||||
domain: entry.domain,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
links = links.filter(Boolean);
|
||||
|
||||
if (this._diagnosticDownloadLinks !== requestId) {
|
||||
return;
|
||||
}
|
||||
links = links.filter(Boolean);
|
||||
if (links.length > 0) {
|
||||
this._diagnosticDownloadLinks = links;
|
||||
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>
|
||||
`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,6 +320,10 @@ export class HaConfigDevicePage extends LitElement {
|
||||
<ha-alert alert-type="warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.enabled_cause",
|
||||
"type",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.type.${device.entry_type || "device"}`
|
||||
),
|
||||
"cause",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.disabled_by.${device.disabled_by}`
|
||||
@@ -485,17 +513,29 @@ export class HaConfigDevicePage extends LitElement {
|
||||
<ha-card>
|
||||
<h1 class="card-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.automations"
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
)}
|
||||
<ha-icon-button
|
||||
@click=${this._showAutomationDialog}
|
||||
.disabled=${device.disabled_by}
|
||||
.label=${device.disabled_by
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.devices.automation.create_disabled"
|
||||
"ui.panel.config.devices.automation.create_disabled",
|
||||
"type",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.type.${
|
||||
device.entry_type || "device"
|
||||
}`
|
||||
)
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.devices.automation.create"
|
||||
"ui.panel.config.devices.automation.create",
|
||||
"type",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.type.${
|
||||
device.entry_type || "device"
|
||||
}`
|
||||
)
|
||||
)}
|
||||
.path=${mdiPlusCircle}
|
||||
></ha-icon-button>
|
||||
@@ -547,6 +587,12 @@ export class HaConfigDevicePage extends LitElement {
|
||||
"name",
|
||||
this.hass.localize(
|
||||
"ui.panel.config.devices.automation.automations"
|
||||
),
|
||||
"type",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.type.${
|
||||
device.entry_type || "device"
|
||||
}`
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
@@ -561,7 +607,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
<ha-card>
|
||||
<h1 class="card-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.scenes"
|
||||
"ui.panel.config.devices.scene.scenes_heading"
|
||||
)}
|
||||
|
||||
<ha-icon-button
|
||||
@@ -569,10 +615,22 @@ export class HaConfigDevicePage extends LitElement {
|
||||
.disabled=${device.disabled_by}
|
||||
.label=${device.disabled_by
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.devices.scene.create_disabled"
|
||||
"ui.panel.config.devices.scene.create_disabled",
|
||||
"type",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.type.${
|
||||
device.entry_type || "device"
|
||||
}`
|
||||
)
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.devices.scene.create"
|
||||
"ui.panel.config.devices.scene.create",
|
||||
"type",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.type.${
|
||||
device.entry_type || "device"
|
||||
}`
|
||||
)
|
||||
)}
|
||||
.path=${mdiPlusCircle}
|
||||
></ha-icon-button>
|
||||
@@ -627,6 +685,12 @@ export class HaConfigDevicePage extends LitElement {
|
||||
"name",
|
||||
this.hass.localize(
|
||||
"ui.panel.config.devices.scene.scenes"
|
||||
),
|
||||
"type",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.type.${
|
||||
device.entry_type || "device"
|
||||
}`
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
@@ -641,17 +705,29 @@ export class HaConfigDevicePage extends LitElement {
|
||||
<ha-card>
|
||||
<h1 class="card-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.script.scripts"
|
||||
"ui.panel.config.devices.script.scripts_heading"
|
||||
)}
|
||||
<ha-icon-button
|
||||
@click=${this._showScriptDialog}
|
||||
.disabled=${device.disabled_by}
|
||||
.label=${device.disabled_by
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.devices.script.create_disabled"
|
||||
"ui.panel.config.devices.script.create_disabled",
|
||||
"type",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.type.${
|
||||
device.entry_type || "device"
|
||||
}`
|
||||
)
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.devices.script.create"
|
||||
"ui.panel.config.devices.script.create",
|
||||
"type",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.type.${
|
||||
device.entry_type || "device"
|
||||
}`
|
||||
)
|
||||
)}
|
||||
.path=${mdiPlusCircle}
|
||||
></ha-icon-button>
|
||||
@@ -685,6 +761,12 @@ export class HaConfigDevicePage extends LitElement {
|
||||
"name",
|
||||
this.hass.localize(
|
||||
"ui.panel.config.devices.script.scripts"
|
||||
),
|
||||
"type",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.type.${
|
||||
device.entry_type || "device"
|
||||
}`
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
@@ -1065,6 +1147,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
align-self: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.column > *:not(:first-child) {
|
||||
|
@@ -338,7 +338,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
${this.hass.localize("ui.panel.config.devices.disabled")}
|
||||
</paper-tooltip>
|
||||
</div>`
|
||||
: "",
|
||||
: "—",
|
||||
};
|
||||
}
|
||||
return columns;
|
||||
|
@@ -74,7 +74,9 @@ export class DialogEntityEditor extends LitElement {
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
.heading=${true}
|
||||
.heading=${stateObj
|
||||
? computeStateName(stateObj)
|
||||
: entry?.name || entityId}
|
||||
hideActions
|
||||
@closed=${this.closeDialog}
|
||||
@close-dialog=${this.closeDialog}
|
||||
|
@@ -42,7 +42,7 @@ import type { HomeAssistant } from "../../../types";
|
||||
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
|
||||
|
||||
const OVERRIDE_DEVICE_CLASSES = {
|
||||
cover: ["window", "door", "garage"],
|
||||
cover: ["window", "door", "garage", "gate"],
|
||||
binary_sensor: ["window", "door", "garage_door", "opening"],
|
||||
};
|
||||
|
||||
|
@@ -76,7 +76,7 @@ export interface StateEntity extends EntityRegistryEntry {
|
||||
}
|
||||
|
||||
export interface EntityRow extends StateEntity {
|
||||
entity: HassEntity;
|
||||
entity?: HassEntity;
|
||||
unavailable: boolean;
|
||||
restored: boolean;
|
||||
status: string;
|
||||
@@ -165,13 +165,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
private _columns = memoize(
|
||||
(narrow, _language, showDisabled): DataTableColumnContainer => ({
|
||||
(narrow, _language, showDisabled): DataTableColumnContainer<EntityRow> => ({
|
||||
icon: {
|
||||
title: "",
|
||||
type: "icon",
|
||||
template: (_, entry: any) => html`
|
||||
template: (_, entry: EntityRow) => html`
|
||||
<ha-state-icon
|
||||
.title=${entry.entity.state}
|
||||
.title=${entry.entity?.state}
|
||||
slot="item-icon"
|
||||
.state=${entry.entity}
|
||||
></ha-state-icon>
|
||||
@@ -186,7 +186,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: narrow
|
||||
? (name, entity: any) =>
|
||||
? (name, entity: EntityRow) =>
|
||||
html`
|
||||
${name}<br />
|
||||
<div class="secondary">
|
||||
@@ -237,7 +237,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
template: (disabled_by) =>
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.disabled_by.${disabled_by}`
|
||||
) || disabled_by,
|
||||
) ||
|
||||
disabled_by ||
|
||||
"—",
|
||||
},
|
||||
status: {
|
||||
title: this.hass.localize(
|
||||
@@ -247,7 +249,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "68px",
|
||||
template: (_status, entity: any) =>
|
||||
template: (_status, entity: EntityRow) =>
|
||||
entity.unavailable || entity.disabled_by || entity.readonly
|
||||
? html`
|
||||
<div
|
||||
|
@@ -12,6 +12,7 @@ import {
|
||||
} from "../../../data/integration";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
|
||||
@customElement("integrations-card")
|
||||
class IntegrationsCard extends LitElement {
|
||||
@@ -66,7 +67,12 @@ class IntegrationsCard extends LitElement {
|
||||
const manifest = this._manifests && this._manifests[domain];
|
||||
const docLink = manifest
|
||||
? html`<a
|
||||
href=${manifest.documentation}
|
||||
href=${manifest.is_built_in
|
||||
? documentationUrl(
|
||||
this.hass,
|
||||
`/integrations/${manifest.domain}`
|
||||
)
|
||||
: manifest.documentation}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize(
|
||||
|
@@ -15,6 +15,7 @@ import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { DataEntryFlowProgressExtended } from "./ha-config-integrations";
|
||||
import "./ha-integration-action-card";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
|
||||
@customElement("ha-config-flow-card")
|
||||
export class HaConfigFlowCard extends LitElement {
|
||||
@@ -82,7 +83,12 @@ export class HaConfigFlowCard extends LitElement {
|
||||
: ""}
|
||||
${this.manifest
|
||||
? html`<a
|
||||
href=${this.manifest.documentation}
|
||||
href=${this.manifest.is_built_in
|
||||
? documentationUrl(
|
||||
this.hass,
|
||||
`/integrations/${this.manifest.domain}`
|
||||
)
|
||||
: this.manifest.documentation}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
|
@@ -46,6 +46,7 @@ import {
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle, haStyleScrollbar } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import type { ConfigEntryExtended } from "./ha-config-integrations";
|
||||
import "./ha-integration-header";
|
||||
@@ -331,7 +332,12 @@ export class HaIntegrationCard extends LitElement {
|
||||
</mwc-list-item>
|
||||
${this.manifest
|
||||
? html` <a
|
||||
href=${this.manifest.documentation}
|
||||
href=${this.manifest.is_built_in
|
||||
? documentationUrl(
|
||||
this.hass,
|
||||
`/integrations/${this.manifest.domain}`
|
||||
)
|
||||
: this.manifest.documentation}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
|
@@ -21,6 +21,7 @@ import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import "../../../ha-config-section";
|
||||
import "../../../../../components/ha-alert";
|
||||
|
||||
export const ozwTabs: PageNavigation[] = [];
|
||||
|
||||
@@ -64,6 +65,30 @@ class OZWConfigDashboard extends LitElement {
|
||||
.tabs=${ozwTabs}
|
||||
back-path="/config/integrations"
|
||||
>
|
||||
<ha-alert
|
||||
alert-type="warning"
|
||||
title="This integration will stop working soon"
|
||||
>
|
||||
The OpenZWave integration is deprecated and will no longer receive any
|
||||
updates. The technical dependencies will render this integration
|
||||
unusable in the near future. We strongly advise you to migrate to the
|
||||
new
|
||||
<a
|
||||
href="https://www.home-assistant.io/integrations/zwave_js"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>Z-Wave JS integration</a
|
||||
>.
|
||||
<a
|
||||
slot="action"
|
||||
href="https://alerts.home-assistant.io/#ozw.markdown"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<mwc-button>learn more</mwc-button>
|
||||
</a>
|
||||
</ha-alert>
|
||||
|
||||
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide}>
|
||||
<div slot="header">
|
||||
${this.hass.localize("ui.panel.config.ozw.select_instance.header")}
|
||||
@@ -162,6 +187,13 @@ class OZWConfigDashboard extends LitElement {
|
||||
:host([narrow]) ha-config-section {
|
||||
margin-top: -20px;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin: 16px;
|
||||
}
|
||||
ha-alert a {
|
||||
text-decoration: none;
|
||||
}
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
@@ -55,6 +55,7 @@ class DialogZHAReconfigureDevice extends LitElement {
|
||||
this._status = undefined;
|
||||
this._stages = undefined;
|
||||
this._clusterConfigurationStatuses = undefined;
|
||||
this._showDetails = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -225,10 +226,10 @@ class DialogZHAReconfigureDevice extends LitElement {
|
||||
)}
|
||||
</h2>
|
||||
|
||||
${this._clusterConfigurationStatuses!.size > 0
|
||||
${this._clusterConfigurationStatuses?.size
|
||||
? html`
|
||||
${Array.from(
|
||||
this._clusterConfigurationStatuses!.values()
|
||||
this._clusterConfigurationStatuses.values()
|
||||
).map(
|
||||
(clusterStatus) => html`
|
||||
<div class="grid-item">
|
||||
|
@@ -12,6 +12,7 @@ import { computeStateName } from "../../../../../common/entity/compute_state_nam
|
||||
import { sortStatesByName } from "../../../../../common/entity/states_sort_by_name";
|
||||
import "../../../../../components/buttons/ha-call-service-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-icon";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-icon-button-arrow-prev";
|
||||
@@ -43,6 +44,14 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin: 16px;
|
||||
}
|
||||
ha-alert a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 24px;
|
||||
}
|
||||
@@ -101,6 +110,30 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
|
||||
<ha-alert
|
||||
alert-type="warning"
|
||||
title="This integration will stop working soon"
|
||||
>
|
||||
This Z-Wave integration is deprecated and will no longer receive any
|
||||
updates. The technical dependencies will render this integration
|
||||
unusable in the near future. We strongly advise you to migrate to the
|
||||
new
|
||||
<a
|
||||
href="https://www.home-assistant.io/integrations/zwave_js"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>Z-Wave JS integration</a
|
||||
>.
|
||||
<a
|
||||
slot="action"
|
||||
href="https://alerts.home-assistant.io/#zwave.markdown"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<mwc-button>learn more</mwc-button>
|
||||
</a>
|
||||
</ha-alert>
|
||||
|
||||
<ha-config-section is-wide="[[isWide]]">
|
||||
<ha-card
|
||||
class="content"
|
||||
|
@@ -21,6 +21,7 @@ import {
|
||||
} from "../../../data/system_log";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import type { SystemLogDetailDialogParams } from "./show-dialog-system-log-detail";
|
||||
import { formatSystemLogTime } from "./util";
|
||||
@@ -68,8 +69,18 @@ class DialogSystemLogDetail extends LitElement {
|
||||
// Custom components with our official docs should not link to our docs
|
||||
!this._manifest.documentation.includes("://www.home-assistant.io"));
|
||||
|
||||
const title = this.hass.localize(
|
||||
"ui.panel.config.logs.details",
|
||||
"level",
|
||||
html`<span class=${item.level.toLowerCase()}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.logs.level." + item.level.toLowerCase()
|
||||
)}</span
|
||||
>`
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog open @closed=${this.closeDialog} hideActions .heading=${true}>
|
||||
<ha-dialog open @closed=${this.closeDialog} hideActions .heading=${title}>
|
||||
<ha-header-bar slot="heading">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
@@ -77,17 +88,7 @@ class DialogSystemLogDetail extends LitElement {
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<span slot="title">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.logs.details",
|
||||
"level",
|
||||
html`<span class=${item.level.toLowerCase()}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.logs.level." + item.level.toLowerCase()
|
||||
)}</span
|
||||
>`
|
||||
)}
|
||||
</span>
|
||||
<span slot="title"> ${title} </span>
|
||||
<ha-icon-button
|
||||
id="copy"
|
||||
@click=${this._copyLog}
|
||||
@@ -117,7 +118,12 @@ class DialogSystemLogDetail extends LitElement {
|
||||
? ""
|
||||
: html`
|
||||
(<a
|
||||
href=${this._manifest.documentation}
|
||||
href=${this._manifest.is_built_in
|
||||
? documentationUrl(
|
||||
this.hass,
|
||||
`/integrations/${this._manifest.domain}`
|
||||
)
|
||||
: this._manifest.documentation}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>documentation</a
|
||||
|
@@ -9,6 +9,10 @@ import { HomeAssistant } from "../../../types";
|
||||
class ErrorLogCard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public filter = "";
|
||||
|
||||
@state() private _isLogLoaded = false;
|
||||
|
||||
@state() private _errorHTML!: TemplateResult[] | string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -43,6 +47,14 @@ class ErrorLogCard extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has("filter") && this._isLogLoaded) {
|
||||
this._refreshErrorLog();
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.error-log-intro {
|
||||
@@ -55,12 +67,16 @@ class ErrorLogCard extends LitElement {
|
||||
}
|
||||
|
||||
.error-log {
|
||||
@apply --paper-font-code)
|
||||
clear: both;
|
||||
font-family: var(--code-font-family, monospace);
|
||||
clear: both;
|
||||
text-align: left;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.error-log > div:hover {
|
||||
background-color: var(--secondary-background-color);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
@@ -74,24 +90,33 @@ class ErrorLogCard extends LitElement {
|
||||
private async _refreshErrorLog(): Promise<void> {
|
||||
this._errorHTML = this.hass.localize("ui.panel.config.logs.loading_log");
|
||||
const log = await fetchErrorLog(this.hass!);
|
||||
this._isLogLoaded = true;
|
||||
|
||||
this._errorHTML = log
|
||||
? log.split("\n").map((entry) => {
|
||||
if (entry.includes("INFO"))
|
||||
return html`<div class="info">${entry}</div>`;
|
||||
? log
|
||||
.split("\n")
|
||||
.filter((entry) => {
|
||||
if (this.filter) {
|
||||
return entry.toLowerCase().includes(this.filter.toLowerCase());
|
||||
}
|
||||
return entry;
|
||||
})
|
||||
.map((entry) => {
|
||||
if (entry.includes("INFO"))
|
||||
return html`<div class="info">${entry}</div>`;
|
||||
|
||||
if (entry.includes("WARNING"))
|
||||
return html`<div class="warning">${entry}</div>`;
|
||||
if (entry.includes("WARNING"))
|
||||
return html`<div class="warning">${entry}</div>`;
|
||||
|
||||
if (
|
||||
entry.includes("ERROR") ||
|
||||
entry.includes("FATAL") ||
|
||||
entry.includes("CRITICAL")
|
||||
)
|
||||
return html`<div class="error">${entry}</div>`;
|
||||
if (
|
||||
entry.includes("ERROR") ||
|
||||
entry.includes("FATAL") ||
|
||||
entry.includes("CRITICAL")
|
||||
)
|
||||
return html`<div class="error">${entry}</div>`;
|
||||
|
||||
return html`<div>${entry}</div>`;
|
||||
})
|
||||
return html`<div>${entry}</div>`;
|
||||
})
|
||||
: this.hass.localize("ui.panel.config.logs.no_errors");
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,11 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "../../../common/search/search-input";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import "./error-log-card";
|
||||
import "./system-log-card";
|
||||
import type { SystemLogCard } from "./system-log-card";
|
||||
@@ -20,6 +22,8 @@ export class HaConfigLogs extends LitElement {
|
||||
|
||||
@property() public route!: Route;
|
||||
|
||||
@state() private _filter = extractSearchParam("filter") ?? "";
|
||||
|
||||
@query("system-log-card", true) private systemLog?: SystemLogCard;
|
||||
|
||||
public connectedCallback() {
|
||||
@@ -29,7 +33,39 @@ export class HaConfigLogs extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _filterChanged(ev) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const search = this.narrow
|
||||
? html`
|
||||
<div slot="header">
|
||||
<search-input
|
||||
class="header"
|
||||
no-label-float
|
||||
no-underline
|
||||
@value-changed=${this._filterChanged}
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
.label=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
></search-input>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="search">
|
||||
<search-input
|
||||
autofocus
|
||||
no-label-float
|
||||
no-underline
|
||||
@value-changed=${this._filterChanged}
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
.label=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
></search-input>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
@@ -38,9 +74,16 @@ export class HaConfigLogs extends LitElement {
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.general}
|
||||
>
|
||||
${search}
|
||||
<div class="content">
|
||||
<system-log-card .hass=${this.hass}></system-log-card>
|
||||
<error-log-card .hass=${this.hass}></error-log-card>
|
||||
<system-log-card
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
></system-log-card>
|
||||
<error-log-card
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
></error-log-card>
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
@@ -56,6 +99,17 @@ export class HaConfigLogs extends LitElement {
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
|
||||
.search {
|
||||
padding: 0 16px;
|
||||
background: var(--sidebar-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.search search-input {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.content {
|
||||
direction: ltr;
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
import "../../../components/buttons/ha-progress-button";
|
||||
import "../../../components/ha-card";
|
||||
@@ -22,6 +23,8 @@ import { formatSystemLogTime } from "./util";
|
||||
export class SystemLogCard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public filter = "";
|
||||
|
||||
public loaded = false;
|
||||
|
||||
@state() private _items?: LoggedError[];
|
||||
@@ -31,9 +34,44 @@ export class SystemLogCard extends LitElement {
|
||||
this._items = await fetchSystemLog(this.hass!);
|
||||
}
|
||||
|
||||
private _timestamp(item: LoggedError): string {
|
||||
return formatSystemLogTime(item.timestamp, this.hass!.locale);
|
||||
}
|
||||
|
||||
private _multipleMessages(item: LoggedError): string {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.logs.multiple_messages",
|
||||
"time",
|
||||
formatSystemLogTime(item.first_occurred, this.hass!.locale),
|
||||
"counter",
|
||||
item.count
|
||||
);
|
||||
}
|
||||
|
||||
private _getFilteredItems = memoizeOne(
|
||||
(items: LoggedError[], filter: string) =>
|
||||
items.filter((item: LoggedError) => {
|
||||
if (filter) {
|
||||
return (
|
||||
item.message.some((message: string) =>
|
||||
message.toLowerCase().includes(filter)
|
||||
) ||
|
||||
item.source[0].toLowerCase().includes(filter) ||
|
||||
item.name.toLowerCase().includes(filter) ||
|
||||
this._timestamp(item).toLowerCase().includes(filter) ||
|
||||
this._multipleMessages(item).toLowerCase().includes(filter)
|
||||
);
|
||||
}
|
||||
return item;
|
||||
})
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const integrations = this._items
|
||||
? this._items.map((item) => getLoggedErrorIntegration(item))
|
||||
const filteredItems = this._items
|
||||
? this._getFilteredItems(this._items, this.filter.toLowerCase())
|
||||
: [];
|
||||
const integrations = filteredItems.length
|
||||
? filteredItems.map((item) => getLoggedErrorIntegration(item))
|
||||
: [];
|
||||
return html`
|
||||
<div class="system-log-intro">
|
||||
@@ -51,17 +89,21 @@ export class SystemLogCard extends LitElement {
|
||||
${this.hass.localize("ui.panel.config.logs.no_issues")}
|
||||
</div>
|
||||
`
|
||||
: this._items.map(
|
||||
: filteredItems.length === 0 && this.filter
|
||||
? html`<div class="card-content">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.logs.no_issues_search",
|
||||
"term",
|
||||
this.filter
|
||||
)}
|
||||
</div>`
|
||||
: filteredItems.map(
|
||||
(item, idx) => html`
|
||||
<paper-item @click=${this._openLog} .logItem=${item}>
|
||||
<paper-item-body two-line>
|
||||
<div class="row">${item.message[0]}</div>
|
||||
<div secondary>
|
||||
${formatSystemLogTime(
|
||||
item.timestamp,
|
||||
this.hass!.locale
|
||||
)}
|
||||
–
|
||||
${this._timestamp(item)} –
|
||||
${html`(<span class=${item.level.toLowerCase()}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.logs.level." +
|
||||
@@ -81,19 +123,7 @@ export class SystemLogCard extends LitElement {
|
||||
}`
|
||||
: item.source[0]}
|
||||
${item.count > 1
|
||||
? html`
|
||||
-
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.logs.multiple_messages",
|
||||
"time",
|
||||
formatSystemLogTime(
|
||||
item.first_occurred,
|
||||
this.hass!.locale
|
||||
),
|
||||
"counter",
|
||||
item.count
|
||||
)}
|
||||
`
|
||||
? html` - ${this._multipleMessages(item)} `
|
||||
: html``}
|
||||
</div>
|
||||
</paper-item-body>
|
||||
|
@@ -7,7 +7,9 @@ import {
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoize from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import { navigate } from "../../../../common/navigate";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import {
|
||||
@@ -52,9 +54,19 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
icon: {
|
||||
title: "",
|
||||
type: "icon",
|
||||
template: (icon) =>
|
||||
template: (icon, dashboard) =>
|
||||
icon
|
||||
? html` <ha-icon slot="item-icon" .icon=${icon}></ha-icon> `
|
||||
? html`
|
||||
<ha-icon
|
||||
slot="item-icon"
|
||||
.icon=${icon}
|
||||
style=${ifDefined(
|
||||
dashboard.iconColor
|
||||
? `color: ${dashboard.iconColor}`
|
||||
: undefined
|
||||
)}
|
||||
></ha-icon>
|
||||
`
|
||||
: html``,
|
||||
},
|
||||
title: {
|
||||
@@ -63,7 +75,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (title, dashboard: any) => {
|
||||
const titleTemplate = html`
|
||||
@@ -183,29 +194,41 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
).mode;
|
||||
const defaultUrlPath = this.hass.defaultPanel;
|
||||
const isDefault = defaultUrlPath === "lovelace";
|
||||
return [
|
||||
const result: Record<string, any>[] = [
|
||||
{
|
||||
icon: "hass:view-dashboard",
|
||||
title: this.hass.localize("panel.states"),
|
||||
default: isDefault,
|
||||
sidebar: isDefault,
|
||||
show_in_sidebar: isDefault,
|
||||
require_admin: false,
|
||||
url_path: "lovelace",
|
||||
mode: defaultMode,
|
||||
filename: defaultMode === "yaml" ? "ui-lovelace.yaml" : "",
|
||||
iconColor: "var(--primary-color)",
|
||||
},
|
||||
{
|
||||
];
|
||||
if (isComponentLoaded(this.hass, "energy")) {
|
||||
result.push({
|
||||
icon: "hass:lightning-bolt",
|
||||
title: this.hass.localize(`ui.panel.config.dashboard.energy.title`),
|
||||
show_in_sidebar: true,
|
||||
mode: "storage",
|
||||
url_path: "energy",
|
||||
filename: "",
|
||||
},
|
||||
...dashboards.map((dashboard) => ({
|
||||
filename: "",
|
||||
...dashboard,
|
||||
default: defaultUrlPath === dashboard.url_path,
|
||||
})),
|
||||
];
|
||||
iconColor: "var(--label-badge-yellow)",
|
||||
});
|
||||
}
|
||||
|
||||
result.push(
|
||||
...dashboards
|
||||
.sort((a, b) => stringCompare(a.title, b.title))
|
||||
.map((dashboard) => ({
|
||||
filename: "",
|
||||
...dashboard,
|
||||
default: defaultUrlPath === dashboard.url_path,
|
||||
}))
|
||||
);
|
||||
return result;
|
||||
});
|
||||
|
||||
protected render(): TemplateResult {
|
||||
|
@@ -42,6 +42,8 @@ class DialogTagDetail
|
||||
this._id = "";
|
||||
this._name = "";
|
||||
}
|
||||
|
||||
this._generateQR();
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
@@ -121,16 +123,9 @@ class DialogTagDetail
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="qr">
|
||||
${this._qrCode
|
||||
? this._qrCode
|
||||
: html`
|
||||
<mwc-button @click=${this._generateQR}
|
||||
>Generate QR code
|
||||
</mwc-button>
|
||||
`}
|
||||
</div>
|
||||
${this._qrCode
|
||||
? html` <div id="qr">${this._qrCode}</div> `
|
||||
: ""}
|
||||
`
|
||||
: ``}
|
||||
</div>
|
||||
@@ -225,6 +220,9 @@ class DialogTagDetail
|
||||
{
|
||||
width: 180,
|
||||
errorCorrectionLevel: "Q",
|
||||
color: {
|
||||
light: "#fff",
|
||||
},
|
||||
}
|
||||
);
|
||||
const context = canvas.getContext("2d");
|
||||
|
@@ -51,7 +51,12 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<p>Step ${this._step + 1} of 5</p>
|
||||
<p>
|
||||
${this.hass.localize("ui.panel.energy.setup.step", {
|
||||
step: this._step + 1,
|
||||
steps: 5,
|
||||
})}
|
||||
</p>
|
||||
${this._step === 0
|
||||
? html`<ha-energy-grid-settings
|
||||
.hass=${this.hass}
|
||||
|
@@ -12,7 +12,11 @@ import {
|
||||
} from "date-fns";
|
||||
import { css, html, LitElement, PropertyValues } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { extractSearchParam } from "../../common/url/search-params";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import {
|
||||
createSearchParam,
|
||||
extractSearchParam,
|
||||
} from "../../common/url/search-params";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import "../../components/chart/state-history-charts";
|
||||
import "../../components/entity/ha-entity-picker";
|
||||
@@ -144,6 +148,10 @@ class HaPanelHistory extends LitElement {
|
||||
if (startDate) {
|
||||
this._startDate = new Date(startDate);
|
||||
}
|
||||
const endDate = extractSearchParam("end_date");
|
||||
if (endDate) {
|
||||
this._endDate = new Date(endDate);
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
@@ -191,10 +199,32 @@ class HaPanelHistory extends LitElement {
|
||||
endDate.setMilliseconds(endDate.getMilliseconds() - 1);
|
||||
}
|
||||
this._endDate = endDate;
|
||||
|
||||
this._updatePath();
|
||||
}
|
||||
|
||||
private _entityPicked(ev) {
|
||||
this._entityId = ev.target.value;
|
||||
|
||||
this._updatePath();
|
||||
}
|
||||
|
||||
private _updatePath() {
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
if (this._entityId) {
|
||||
params.entity_id = this._entityId;
|
||||
}
|
||||
|
||||
if (this._startDate) {
|
||||
params.start_date = this._startDate.toISOString();
|
||||
}
|
||||
|
||||
if (this._endDate) {
|
||||
params.end_date = this._endDate.toISOString();
|
||||
}
|
||||
|
||||
navigate(`/history?${createSearchParam(params)}`, { replace: true });
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import { mdiRefresh } from "@mdi/js";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import { css, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import {
|
||||
addDays,
|
||||
endOfToday,
|
||||
@@ -12,8 +10,15 @@ import {
|
||||
startOfWeek,
|
||||
startOfYesterday,
|
||||
} from "date-fns";
|
||||
import { css, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import {
|
||||
createSearchParam,
|
||||
extractSearchParam,
|
||||
} from "../../common/url/search-params";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import "../../components/entity/ha-entity-picker";
|
||||
import "../../components/ha-circular-progress";
|
||||
@@ -33,7 +38,6 @@ import "../../layouts/ha-app-layout";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "./ha-logbook";
|
||||
import { extractSearchParam } from "../../common/url/search-params";
|
||||
|
||||
@customElement("ha-panel-logbook")
|
||||
export class HaPanelLogbook extends LitElement {
|
||||
@@ -166,6 +170,10 @@ export class HaPanelLogbook extends LitElement {
|
||||
if (startDate) {
|
||||
this._startDate = new Date(startDate);
|
||||
}
|
||||
const endDate = extractSearchParam("end_date");
|
||||
if (endDate) {
|
||||
this._endDate = new Date(endDate);
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues<this>) {
|
||||
@@ -223,10 +231,32 @@ export class HaPanelLogbook extends LitElement {
|
||||
endDate.setMilliseconds(endDate.getMilliseconds() - 1);
|
||||
}
|
||||
this._endDate = endDate;
|
||||
|
||||
this._updatePath();
|
||||
}
|
||||
|
||||
private _entityPicked(ev) {
|
||||
this._entityId = ev.target.value;
|
||||
|
||||
this._updatePath();
|
||||
}
|
||||
|
||||
private _updatePath() {
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
if (this._entityId) {
|
||||
params.entity_id = this._entityId;
|
||||
}
|
||||
|
||||
if (this._startDate) {
|
||||
params.start_date = this._startDate.toISOString();
|
||||
}
|
||||
|
||||
if (this._endDate) {
|
||||
params.end_date = this._endDate.toISOString();
|
||||
}
|
||||
|
||||
navigate(`/logbook?${createSearchParam(params)}`, { replace: true });
|
||||
}
|
||||
|
||||
private _refreshLogbook() {
|
||||
|
@@ -170,6 +170,21 @@ export class HuiEnergyDevicesGraphCard
|
||||
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
|
||||
);
|
||||
|
||||
const startMinHour = addHours(energyData.start, -1);
|
||||
|
||||
Object.values(this._data).forEach((stat) => {
|
||||
// if the start of the first value is after the requested period, we have the first data point, and should add a zero point
|
||||
if (stat.length && new Date(stat[0].start) > startMinHour) {
|
||||
stat.unshift({
|
||||
...stat[0],
|
||||
start: startMinHour.toISOString(),
|
||||
end: startMinHour.toISOString(),
|
||||
sum: 0,
|
||||
state: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const data: Array<ChartDataset<"bar", ParsedDataType<"bar">>["data"]> = [];
|
||||
const borderColor: string[] = [];
|
||||
const backgroundColor: string[] = [];
|
||||
|
@@ -23,8 +23,8 @@ import { iconColorCSS } from "../../../common/style/icon_color_css";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
import { UNAVAILABLE_STATES } from "../../../data/entity";
|
||||
import { formatAttributeValue } from "../../../data/entity_attributes";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { formatAttributeValue } from "../../../util/hass-attributes-util";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
|
@@ -106,6 +106,9 @@ export class HuiImage extends LitElement {
|
||||
} else if (!this.hass!.connected) {
|
||||
this._stopUpdateCameraInterval();
|
||||
this._stopIntersectionObserver();
|
||||
this._loadState = LoadState.Loading;
|
||||
this._cameraImageSrc = undefined;
|
||||
this._loadedImageSrc = undefined;
|
||||
}
|
||||
}
|
||||
if (changedProps.has("_imageVisible")) {
|
||||
|
@@ -65,28 +65,26 @@ export class HuiCreateDialogCard
|
||||
return html``;
|
||||
}
|
||||
|
||||
const title = this._viewConfig.title
|
||||
? this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.pick_card_view_title",
|
||||
"name",
|
||||
`"${this._viewConfig.title}"`
|
||||
)
|
||||
: this.hass!.localize("ui.panel.lovelace.editor.edit_card.pick_card");
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
@keydown=${this._ignoreKeydown}
|
||||
@closed=${this._cancel}
|
||||
.heading=${true}
|
||||
.heading=${title}
|
||||
class=${classMap({ table: this._currTabIndex === 1 })}
|
||||
>
|
||||
<div slot="heading">
|
||||
<ha-header-bar>
|
||||
<span slot="title">
|
||||
${this._viewConfig.title
|
||||
? this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.pick_card_view_title",
|
||||
"name",
|
||||
`"${this._viewConfig.title}"`
|
||||
)
|
||||
: this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.pick_card"
|
||||
)}
|
||||
</span>
|
||||
<span slot="title"> ${title} </span>
|
||||
</ha-header-bar>
|
||||
<mwc-tab-bar
|
||||
.activeIndex=${this._currTabIndex}
|
||||
|
@@ -176,7 +176,7 @@ export class HuiDialogEditCard
|
||||
@keydown=${this._ignoreKeydown}
|
||||
@closed=${this._cancel}
|
||||
@opened=${this._opened}
|
||||
.heading=${true}
|
||||
.heading=${heading}
|
||||
>
|
||||
<div slot="heading">
|
||||
<ha-header-bar>
|
||||
|
@@ -173,7 +173,7 @@ export class HuiDialogEditView extends LitElement {
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${true}
|
||||
.heading=${this._viewConfigTitle}
|
||||
>
|
||||
<div slot="heading">
|
||||
<h2>${this._viewConfigTitle}</h2>
|
||||
|
@@ -130,6 +130,7 @@ class HUIRoot extends LitElement {
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<mwc-button
|
||||
outlined
|
||||
class="exit-edit-mode"
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.menu.exit_edit_mode"
|
||||
@@ -938,7 +939,8 @@ class HUIRoot extends LitElement {
|
||||
);
|
||||
}
|
||||
.exit-edit-mode {
|
||||
--mdc-theme-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--app-header-edit-text-color, #fff);
|
||||
--mdc-button-outline-color: var(--app-header-edit-text-color, #fff);
|
||||
--mdc-typography-button-font-size: 14px;
|
||||
}
|
||||
`,
|
||||
|
@@ -9,8 +9,8 @@ import {
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import checkValidDate from "../../../common/datetime/check_valid_date";
|
||||
import { formatNumber } from "../../../common/number/format_number";
|
||||
import { formatAttributeValue } from "../../../data/entity_attributes";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { formatAttributeValue } from "../../../util/hass-attributes-util";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-generic-entity-row";
|
||||
import "../components/hui-timestamp-display";
|
||||
|
@@ -3,7 +3,7 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { subscribeOne } from "../../../common/util/subscribe-one";
|
||||
import { subscribeAreaRegistry } from "../../../data/area_registry";
|
||||
import { subscribeDeviceRegistry } from "../../../data/device_registry";
|
||||
import { EnergyPreferences, getEnergyPreferences } from "../../../data/energy";
|
||||
import { getEnergyPreferences } from "../../../data/energy";
|
||||
import { subscribeEntityRegistry } from "../../../data/entity_registry";
|
||||
import { generateDefaultViewConfig } from "../common/generate-lovelace-config";
|
||||
import {
|
||||
@@ -39,30 +39,18 @@ export class OriginalStatesStrategy {
|
||||
subscribeEntityRegistry(hass.connection, () => undefined);
|
||||
}
|
||||
|
||||
let energyPromise: Promise<EnergyPreferences> | undefined;
|
||||
|
||||
if (isComponentLoaded(hass, "energy")) {
|
||||
energyPromise = getEnergyPreferences(hass);
|
||||
}
|
||||
|
||||
const [areaEntries, deviceEntries, entityEntries, localize] =
|
||||
const [areaEntries, deviceEntries, entityEntries, localize, energyPrefs] =
|
||||
await Promise.all([
|
||||
subscribeOne(hass.connection, subscribeAreaRegistry),
|
||||
subscribeOne(hass.connection, subscribeDeviceRegistry),
|
||||
subscribeOne(hass.connection, subscribeEntityRegistry),
|
||||
hass.loadBackendTranslation("title"),
|
||||
isComponentLoaded(hass, "energy")
|
||||
? // It raises if not configured, just swallow that.
|
||||
getEnergyPreferences(hass).catch(() => undefined)
|
||||
: undefined,
|
||||
]);
|
||||
|
||||
let energyPrefs: EnergyPreferences | undefined;
|
||||
|
||||
if (energyPromise) {
|
||||
try {
|
||||
energyPrefs = await energyPromise;
|
||||
} catch (_) {
|
||||
// Nothing to do here
|
||||
}
|
||||
}
|
||||
|
||||
// User can override default view. If they didn't, we will add one
|
||||
// that contains all entities.
|
||||
const view = generateDefaultViewConfig(
|
||||
|
@@ -19,6 +19,7 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
@@ -114,7 +115,9 @@ class BarMediaPlayer extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
const isBrowser = this.entityId === BROWSER_PLAYER;
|
||||
const stateObj = this._stateObj;
|
||||
const controls = !this.narrow
|
||||
const controls = !stateObj
|
||||
? undefined
|
||||
: !this.narrow
|
||||
? computeMediaControls(stateObj)
|
||||
: (stateObj.state === "playing" &&
|
||||
(supportsFeature(stateObj, SUPPORT_PAUSE) ||
|
||||
@@ -143,16 +146,22 @@ class BarMediaPlayer extends LitElement {
|
||||
},
|
||||
]
|
||||
: [{}];
|
||||
const mediaDescription = computeMediaDescription(stateObj);
|
||||
const mediaDuration = formatMediaTime(stateObj!.attributes.media_duration!);
|
||||
const mediaTitleClean = cleanupMediaTitle(stateObj.attributes.media_title);
|
||||
const mediaDescription = stateObj ? computeMediaDescription(stateObj) : "";
|
||||
const mediaDuration = formatMediaTime(stateObj?.attributes.media_duration);
|
||||
const mediaTitleClean = cleanupMediaTitle(
|
||||
stateObj?.attributes.media_title || ""
|
||||
);
|
||||
|
||||
const mediaArt =
|
||||
stateObj.attributes.entity_picture_local ||
|
||||
stateObj.attributes.entity_picture;
|
||||
const mediaArt = stateObj
|
||||
? stateObj.attributes.entity_picture_local ||
|
||||
stateObj.attributes.entity_picture
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<div class="info">
|
||||
<div
|
||||
class="info ${!isBrowser ? "pointer" : ""}"
|
||||
@click=${this._openMoreInfo}
|
||||
>
|
||||
${mediaArt ? html`<img src=${this.hass.hassUrl(mediaArt)} />` : ""}
|
||||
<div class="media-info">
|
||||
<hui-marquee
|
||||
@@ -212,7 +221,7 @@ class BarMediaPlayer extends LitElement {
|
||||
slot="trigger"
|
||||
.label=${this.narrow
|
||||
? ""
|
||||
: `${computeStateName(stateObj)}
|
||||
: `${stateObj ? computeStateName(stateObj) : this.entityId}
|
||||
`}
|
||||
>
|
||||
<ha-svg-icon
|
||||
@@ -284,7 +293,7 @@ class BarMediaPlayer extends LitElement {
|
||||
if (
|
||||
!this._progressInterval &&
|
||||
this._showProgressBar &&
|
||||
stateObj.state === "playing"
|
||||
stateObj?.state === "playing"
|
||||
) {
|
||||
this._progressInterval = window.setInterval(
|
||||
() => this._updateProgressBar(),
|
||||
@@ -292,21 +301,27 @@ class BarMediaPlayer extends LitElement {
|
||||
);
|
||||
} else if (
|
||||
this._progressInterval &&
|
||||
(!this._showProgressBar || stateObj.state !== "playing")
|
||||
(!this._showProgressBar || stateObj?.state !== "playing")
|
||||
) {
|
||||
clearInterval(this._progressInterval);
|
||||
this._progressInterval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private get _stateObj(): MediaPlayerEntity {
|
||||
if (this._browserPlayer) {
|
||||
return this._browserPlayer.toStateObj();
|
||||
private get _stateObj(): MediaPlayerEntity | undefined {
|
||||
if (this.entityId === BROWSER_PLAYER) {
|
||||
return this._browserPlayer
|
||||
? this._browserPlayer.toStateObj()
|
||||
: BrowserMediaPlayer.idleStateObj();
|
||||
}
|
||||
return (
|
||||
(this.hass!.states[this.entityId] as MediaPlayerEntity | undefined) ||
|
||||
BrowserMediaPlayer.idleStateObj()
|
||||
);
|
||||
return this.hass!.states[this.entityId] as MediaPlayerEntity | undefined;
|
||||
}
|
||||
|
||||
private _openMoreInfo() {
|
||||
if (this._browserPlayer) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "hass-more-info", { entityId: this.entityId });
|
||||
}
|
||||
|
||||
private get _showProgressBar() {
|
||||
@@ -317,6 +332,7 @@ class BarMediaPlayer extends LitElement {
|
||||
const stateObj = this._stateObj;
|
||||
|
||||
return (
|
||||
stateObj &&
|
||||
(stateObj.state === "playing" || stateObj.state === "paused") &&
|
||||
"media_duration" in stateObj.attributes &&
|
||||
"media_position" in stateObj.attributes
|
||||
@@ -332,19 +348,21 @@ class BarMediaPlayer extends LitElement {
|
||||
}
|
||||
|
||||
private _updateProgressBar(): void {
|
||||
if (!this._progressBar || !this._currentProgress) {
|
||||
const stateObj = this._stateObj;
|
||||
|
||||
if (!this._progressBar || !this._currentProgress || !stateObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._stateObj.attributes.media_duration) {
|
||||
if (!stateObj.attributes.media_duration) {
|
||||
this._progressBar.progress = 0;
|
||||
this._currentProgress.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const currentProgress = getCurrentProgress(this._stateObj);
|
||||
const currentProgress = getCurrentProgress(stateObj);
|
||||
this._progressBar.progress =
|
||||
currentProgress / this._stateObj.attributes.media_duration;
|
||||
currentProgress / stateObj.attributes.media_duration;
|
||||
|
||||
if (this._currentProgress) {
|
||||
this._currentProgress.innerHTML = formatMediaTime(currentProgress);
|
||||
@@ -418,6 +436,10 @@ class BarMediaPlayer extends LitElement {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.secondary,
|
||||
.progress {
|
||||
color: var(--secondary-text-color);
|
||||
@@ -433,6 +455,7 @@ class BarMediaPlayer extends LitElement {
|
||||
|
||||
.controls {
|
||||
height: 48px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.controls-progress {
|
||||
@@ -486,6 +509,7 @@ class BarMediaPlayer extends LitElement {
|
||||
|
||||
:host([narrow]) .controls {
|
||||
display: flex;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
:host([narrow]) .choose-player {
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { mdiArrowLeft } from "@mdi/js";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import {
|
||||
@@ -13,15 +14,21 @@ import { LocalStorage } from "../../common/decorators/local-storage";
|
||||
import { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import "../../components/ha-menu-button";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/media-player/ha-media-player-browse";
|
||||
import type { MediaPlayerItemId } from "../../components/media-player/ha-media-player-browse";
|
||||
import { BROWSER_PLAYER, MediaPickedEvent } from "../../data/media-player";
|
||||
import {
|
||||
BROWSER_PLAYER,
|
||||
MediaPickedEvent,
|
||||
MediaPlayerItem,
|
||||
} from "../../data/media-player";
|
||||
import { resolveMediaSource } from "../../data/media_source";
|
||||
import "../../layouts/ha-app-layout";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../types";
|
||||
import "./ha-bar-media-player";
|
||||
import { showWebBrowserPlayMediaDialog } from "./show-media-player-dialog";
|
||||
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
|
||||
@customElement("ha-panel-media-browser")
|
||||
class PanelMediaBrowser extends LitElement {
|
||||
@@ -32,6 +39,8 @@ class PanelMediaBrowser extends LitElement {
|
||||
|
||||
@property() public route!: Route;
|
||||
|
||||
@property() _currentItem?: MediaPlayerItem;
|
||||
|
||||
private _navigateIds: MediaPlayerItemId[] = [
|
||||
{
|
||||
media_content_id: undefined,
|
||||
@@ -47,15 +56,26 @@ class PanelMediaBrowser extends LitElement {
|
||||
<ha-app-layout>
|
||||
<app-header fixed slot="header">
|
||||
<app-toolbar>
|
||||
<ha-menu-button
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
${this._navigateIds.length > 1
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${mdiArrowLeft}
|
||||
@click=${this._goBack}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: html`
|
||||
<ha-menu-button
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`}
|
||||
<div main-title class="heading">
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.media-player-browser"
|
||||
)}
|
||||
${!this._currentItem
|
||||
? this.hass.localize(
|
||||
"ui.components.media-browser.media-player-browser"
|
||||
)
|
||||
: this._currentItem.title}
|
||||
</div>
|
||||
</div>
|
||||
</app-toolbar>
|
||||
@@ -93,6 +113,23 @@ class PanelMediaBrowser extends LitElement {
|
||||
.split("/");
|
||||
|
||||
if (routePlayer !== this._entityId) {
|
||||
// Detect if picked player doesn't exist (anymore)
|
||||
// Can happen if URL bookmarked or stored in local storage
|
||||
if (
|
||||
routePlayer !== BROWSER_PLAYER &&
|
||||
this.hass.states[routePlayer] === undefined
|
||||
) {
|
||||
navigate(`/media-browser/${BROWSER_PLAYER}`, { replace: true });
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.media-browser.error.player_not_exist",
|
||||
{
|
||||
name: routePlayer,
|
||||
}
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._entityId = routePlayer;
|
||||
}
|
||||
|
||||
@@ -110,13 +147,19 @@ class PanelMediaBrowser extends LitElement {
|
||||
};
|
||||
}),
|
||||
];
|
||||
this._currentItem = undefined;
|
||||
}
|
||||
|
||||
private _mediaBrowsed(ev) {
|
||||
if (ev.detail.back) {
|
||||
history.back();
|
||||
private _goBack() {
|
||||
history.back();
|
||||
}
|
||||
|
||||
private _mediaBrowsed(ev: { detail: HASSDomEvents["media-browsed"] }) {
|
||||
if (ev.detail.ids === this._navigateIds) {
|
||||
this._currentItem = ev.detail.current;
|
||||
return;
|
||||
}
|
||||
|
||||
let path = "";
|
||||
for (const item of ev.detail.ids.slice(1)) {
|
||||
path +=
|
||||
@@ -152,6 +195,7 @@ class PanelMediaBrowser extends LitElement {
|
||||
sourceUrl: resolvedUrl.url,
|
||||
sourceType: resolvedUrl.mime_type,
|
||||
title: item.title,
|
||||
can_play: item.can_play,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ export interface WebBrowserPlayMediaDialogParams {
|
||||
sourceUrl: string;
|
||||
sourceType: string;
|
||||
title?: string;
|
||||
can_play?: boolean;
|
||||
}
|
||||
|
||||
export const showWebBrowserPlayMediaDialog = (
|
||||
|
@@ -65,7 +65,7 @@ export class HaLongLivedAccessTokenDialog extends LitElement {
|
||||
|
||||
private async _generateQR() {
|
||||
const qrcode = await import("qrcode");
|
||||
const canvas = await qrcode.toCanvas(this._params?.token, {
|
||||
const canvas = await qrcode.toCanvas(this._params!.token, {
|
||||
width: 180,
|
||||
errorCorrectionLevel: "Q",
|
||||
});
|
||||
|
@@ -1,3 +1,4 @@
|
||||
// Caution before editing - For latest builds, this module is replaced with emptiness and thus not imported (see build-scripts/bundle.js)
|
||||
import "core-js";
|
||||
import "regenerator-runtime/runtime";
|
||||
import "lit/polyfill-support";
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user