Compare commits

...

68 Commits

Author SHA1 Message Date
Paulus Schoutsen
3717e94814 Switch onboarding and auth to dynamic import 2020-10-12 14:10:15 +00:00
Tobias Kündig
993d73c359 Added entity_id to history graph tooltip (#7310)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-10-12 14:12:54 +02:00
Philip Allgaier
97ca0b818e Capitalize first character of attributes (#7313) 2020-10-12 11:43:46 +02:00
Thomas Lovén
44166f76d4 Scriptomation yaml editor (#7273) 2020-10-12 11:26:16 +02:00
Nico Hirsch
557d6d37a1 Fix charts tooltip (#7216) 2020-10-12 11:12:20 +02:00
Paulus Schoutsen
d3ad56a307 Update compatibility and fix polyfills for ES5 (#7298) 2020-10-12 10:48:33 +02:00
Mischa Gruber
0641022ec5 Use translations for alexa (#7301) 2020-10-12 10:28:11 +02:00
Zack Barett
80c7a8473a Fix Entity Toggle not working for Type Row (#7289) 2020-10-12 10:26:23 +02:00
Matt
d9a954ca91 Close notification drawer after dismissing last notification (#7229)
* Close notification drawer after dismissing last

This change adds a listener to any changes of the notifications property once the drawer opens. After the last notification is dismissed, the notification drawer closes automatically, and the listener is removed.

* Use observer instead event for notification change

Using the observer pattern instead subscribing to change events for notification changes simplifies the implementation noticeably.
2020-10-12 10:25:04 +02:00
Donnie
c219f64322 Rename 'quick open dialog' to 'quick bar' (#7286) 2020-10-12 10:18:18 +02:00
HomeAssistant Azure
f7a9ecff21 [ci skip] Translation update 2020-10-12 00:32:48 +00:00
Jonas Bröms
2b3126ae04 hassio-addon-info.ts: Fix spelling (#7311) 2020-10-11 15:21:02 -05:00
Kyle Niewiada
934c227545 Sort profile refresh tokens by 'last used at' date (#4484) (#7199) 2020-10-11 01:17:05 -05:00
Villhellm
cc0515c217 Add help link on automations picker and updated links for scripts and scenes (#7129)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2020-10-11 01:14:22 -05:00
HomeAssistant Azure
55ba75f2bc [ci skip] Translation update 2020-10-11 00:32:41 +00:00
alex6480
c220228566 Add link to documentation for persons (#7205)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2020-10-10 16:19:59 -05:00
Daniel Martin Gonzalez
26b476ab3c Weather card: Add wind speed direction (#7202)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2020-10-10 16:19:40 -05:00
Paulus Schoutsen
b8a67d530f Update translations 2020-10-10 16:41:36 +02:00
HomeAssistant Azure
b08c96d2db [ci skip] Translation update 2020-10-10 00:32:54 +00:00
Philip Allgaier
4773c39a57 Ensure ha-dialog uses correct <a> color (#7255) 2020-10-09 11:29:47 +02:00
Ryan Meek
892843b290 Supervisor disk usage more info (#7247)
Co-authored-by: Joakim Sørensen <ludeeus@gmail.com>
2020-10-09 10:15:55 +02:00
HomeAssistant Azure
733244531e [ci skip] Translation update 2020-10-09 00:32:37 +00:00
Bram Kragten
66633273e2 Fix history chart fetching changes (#7235) 2020-10-08 16:41:25 +02:00
Spencer Williams
0405adcd16 Edit Person button in the Person "more info" dialog (fixes #4706) (#7208)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2020-10-08 16:40:55 +02:00
Ian Richardson
426a7ac8dd Show moon phase icon in state-label-badge (#7194)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-10-08 16:37:51 +02:00
Thomas Lovén
3bf6205ff7 Display qr code in tag properties (#7092) 2020-10-08 16:37:17 +02:00
Thomas Lovén
c7f4986e61 Put automation/script editor actions in a menu (#7250)
* Put automation/script editor actions in a menu

* Use disabled property instead of attribute
2020-10-08 16:37:01 +02:00
Gilson Marquato Júnior
0f0a3fdaf7 Add keyboard shortcut to save automation/scene/script (#7207) 2020-10-08 16:24:08 +02:00
Bram Kragten
7d6911b140 Replace mdc circular progress with mwc (#7248) 2020-10-08 16:17:20 +02:00
Jaroslav Hanslík
b8777539d7 Fixed localization of relative time (#7256) 2020-10-08 15:53:59 +02:00
Joakim Sørensen
5fc0eaef1a Warn about slow snapshot downloads (#7265) 2020-10-08 15:06:42 +02:00
Paulus Schoutsen
113718c3c1 Do not show weather forecast in generated UI (#7251) 2020-10-08 13:29:13 +02:00
Ryan Meek
701bea6cae Fix tab focus issue in entity picker and password form. (#7252) 2020-10-08 13:24:58 +02:00
Donnie
8d516ed12a Add "quick open" style dialog for selecting entities and running reload commands (#7230)
Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-10-08 13:20:57 +02:00
Joakim Sørensen
667c5744f2 Fix ha-bar display issue on firefox (#7263) 2020-10-08 12:29:21 +02:00
Bram Kragten
80b7c840e2 Fix cloud webhooks (#7261) 2020-10-08 10:33:06 +02:00
HomeAssistant Azure
919c86796f [ci skip] Translation update 2020-10-08 00:32:28 +00:00
Bram Kragten
c90c88ecbf Bump typescript (4) and babel (#7249) 2020-10-07 17:58:56 +02:00
Paulus Schoutsen
d9ba0e2c46 Upgrade to Webpack 5 (#6200) 2020-10-07 10:54:42 +02:00
HomeAssistant Azure
45b2fc590b [ci skip] Translation update 2020-10-07 00:32:14 +00:00
Daniel
17ffdb0247 Update script editor panel tooltips (#7204) 2020-10-06 20:44:20 +02:00
J. Nick Koston
c2fba15fc6 Avoid fetching logbook when there will never be entries (#7239) 2020-10-06 11:22:38 -05:00
Bram Kragten
5937be695f Bump Lit, use cache for query (#7245) 2020-10-06 15:55:55 +02:00
Tomasz
a076fcde84 replace ha-icon_button and ha-icon in stack card editor (#7233) 2020-10-06 12:50:06 +02:00
Bram Kragten
ede9931903 Only do 1 token update a time (#7236) 2020-10-06 10:55:12 +02:00
Paul Klingelhuber
722e01608c Fix flickering scrollbar when using progress-spinner (#7237) 2020-10-06 09:47:25 +02:00
HomeAssistant Azure
af926370d6 [ci skip] Translation update 2020-10-06 00:32:53 +00:00
Tomasz
5971aee02e dot notation for path property of ha-svg-icon (#7197) 2020-10-05 19:51:14 +02:00
Bram Kragten
3940606167 Fix Apple not showing cards (#7232) 2020-10-05 15:54:32 +02:00
David Beitey
da9faccada Allow viewport scaling (zooming) of frontend (#7180)
The inclusion of the `user-scalable=no` value in the viewport meta tag
prevented viewport scaling, disabling the ability to zoom the webpage.
This most typically affects mobile devices, given the nature of the
`<meta name="viewport">` tag.

Removing the restriction allows a user to zoom in to see small and fine
detail in the UI -- such as zooming in on particular areas of a home
security camera streams or other images, inspecting detail in state and
other graphs, and so on.

For users with accessibility requirements, such as low vision
conditions, being able to zoom the frontend means they can enlarge UI
elements to suit them (MDN explains several accessibility concerns at
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name#Accessibility_concerns_with_viewport_scaling)

This change has no effect on users that choose not to use it (for
example, only those that engage zooming such as via pinch-to-zoom on
mobile devices will see the change) -- the frontend remains the same
otherwise.  Elements of the frontend that do use pinch-to-zoom (e.g. the
Map) continue to work as expected, with pinches on that screen area
being captured by the map.
2020-10-05 13:29:07 +02:00
Ryan Meek
7e708b3bf7 Use consistent title case for headers in Supervisor (#7227) 2020-10-05 12:05:57 +02:00
Ryan Meek
05630c9896 Use client_id if no client_name in delete dialog (#7228) 2020-10-05 10:24:23 +02:00
HomeAssistant Azure
369c56db73 [ci skip] Translation update 2020-10-05 00:32:49 +00:00
Ryan Meek
9873459169 Styling fixes for hui-masonry-view (#7226) 2020-10-04 15:15:14 -05:00
HomeAssistant Azure
7776b3766b [ci skip] Translation update 2020-10-04 00:32:25 +00:00
HomeAssistant Azure
29c9004654 [ci skip] Translation update 2020-10-03 00:32:26 +00:00
Zack Barett
601c909004 Warning Element: Fix Overflow in Entity Row (#7193) 2020-10-02 11:29:29 -05:00
Tomasz
72aa9a3b62 use ha-svg-icon in more-info-weather (#7196) 2020-10-02 15:12:54 +02:00
Bram Kragten
2ecf7bca97 Set hass when creating card (#7187) 2020-10-02 15:09:27 +02:00
Zack Barett
cbdfaccdb2 Logbook: Fix for no state obj (#7191) 2020-10-02 14:57:42 +02:00
Zack Barett
93d1b9a2d5 Fix entities Card toggle (#7192) 2020-10-02 10:12:55 +02:00
HomeAssistant Azure
bfb5ee794e [ci skip] Translation update 2020-10-02 00:32:24 +00:00
J. Nick Koston
9ae8bd238b Fix reversal in power icon (#7188) 2020-10-01 11:24:13 -05:00
Bram Kragten
0171f3aec7 Bumped version to 20201001.0 2020-10-01 15:42:07 +02:00
Zack Barett
2c827bab9a Logbook: fix custom Icon (#7175) 2020-10-01 13:55:27 +02:00
Bram Kragten
ec920093d4 Fix panel view and backgrounds (#7181) 2020-10-01 13:54:54 +02:00
HomeAssistant Azure
4289ff6652 [ci skip] Translation update 2020-10-01 00:32:20 +00:00
Charles Garwood
cd32ef60da Show error if no OZW instances are detected (#7176) 2020-09-30 23:09:48 +02:00
206 changed files with 9279 additions and 2755 deletions

View File

@@ -76,12 +76,13 @@
"default-case": 0,
"wc/no-self-class": 0,
"@typescript-eslint/camelcase": 0,
"@typescript-eslint/ban-ts-ignore": 0,
"@typescript-eslint/ban-ts-comment": 0,
"@typescript-eslint/no-use-before-define": 0,
"@typescript-eslint/no-non-null-assertion": 0,
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/no-unused-vars": 0,
"@typescript-eslint/explicit-function-return-type": 0
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/explicit-module-boundary-types": 0
},
"plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"],
"processor": "disable/disable"

View File

@@ -52,7 +52,10 @@ module.exports.terserOptions = (latestBuild) => ({
module.exports.babelOptions = ({ latestBuild }) => ({
babelrc: false,
presets: [
!latestBuild && [require("@babel/preset-env").default, { modules: false }],
!latestBuild && [
require("@babel/preset-env").default,
{ modules: false, useBuiltIns: "entry", corejs: 3 },
],
require("@babel/preset-typescript").default,
].filter(Boolean),
plugins: [
@@ -62,6 +65,7 @@ module.exports.babelOptions = ({ latestBuild }) => ({
{ loose: true, useBuiltIns: true },
],
// Only support the syntax, Webpack will handle it.
"@babel/plugin-syntax-import-meta",
"@babel/syntax-dynamic-import",
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator",

View File

@@ -2,7 +2,6 @@ const webpack = require("webpack");
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
const ManifestPlugin = require("webpack-manifest-plugin");
const WorkerPlugin = require("worker-plugin");
const paths = require("./paths.js");
const bundle = require("./bundle");
@@ -55,7 +54,6 @@ const createWebpackConfig = ({
],
},
plugins: [
new WorkerPlugin(),
new ManifestPlugin({
// Only include the JS of entrypoints
filter: (file) => file.isInitial && !file.name.endsWith(".map"),

View File

@@ -45,6 +45,8 @@ const showMediaPlayer = () => {
style.innerHTML = `
body {
--logo-image: url('https://www.home-assistant.io/images/home-assistant-logo.svg');
--logo-repeat: no-repeat;
--playback-logo-image: url('https://www.home-assistant.io/images/home-assistant-logo.svg');
--theme-hue: 200;
--progress-color: #03a9f4;
--splash-image: url('https://home-assistant.io/images/cast/splash.png');

View File

@@ -49,7 +49,6 @@ class HcLovelace extends LitElement {
.hass=${this.hass}
.lovelace=${lovelace}
.index=${index}
columns="2"
></hui-view>
`;
}
@@ -67,7 +66,7 @@ class HcLovelace extends LitElement {
if (configBackground) {
(this.shadowRoot!.querySelector(
"hui-view, hui-panel-view"
"hui-view"
) as HTMLElement)!.style.setProperty(
"--lovelace-background",
configBackground

View File

@@ -98,14 +98,14 @@ class HassioAddonStore extends LitElement {
main-page
.tabs=${supervisorTabs}
>
<span slot="header">Add-on store</span>
<span slot="header">Add-on Store</span>
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@action=${this._handleAction}
>
<mwc-icon-button slot="trigger" alt="menu">
<ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon>
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item>
Repositories

View File

@@ -39,7 +39,7 @@ class HassioAddonConfig extends LitElement {
@property({ type: Boolean }) private _configHasChanged = false;
@query("ha-yaml-editor") private _editor!: HaYamlEditor;
@query("ha-yaml-editor", true) private _editor!: HaYamlEditor;
protected render(): TemplateResult {
const editor = this._editor;

View File

@@ -69,7 +69,7 @@ const STAGE_ICON = {
const PERMIS_DESC = {
stage: {
title: "Add-on Stage",
description: `Add-ons can have one of three stages:\n\n<ha-svg-icon path='${STAGE_ICON.stable}'></ha-svg-icon> **Stable**: These are add-ons ready to be used in production.\n\n<ha-svg-icon path='${STAGE_ICON.experimental}'></ha-svg-icon> **Experimental**: These may contain bugs, and may be unfinished.\n\n<ha-svg-icon path='${STAGE_ICON.deprecated}'></ha-svg-icon> **Deprecated**: These add-ons will no longer receive any updates.`,
description: `Add-ons can have one of three stages:\n\n<ha-svg-icon .path='${STAGE_ICON.stable}'></ha-svg-icon> **Stable**: These are add-ons ready to be used in production.\n\n<ha-svg-icon .path='${STAGE_ICON.experimental}'></ha-svg-icon> **Experimental**: These may contain bugs, and may be unfinished.\n\n<ha-svg-icon .path='${STAGE_ICON.deprecated}'></ha-svg-icon> **Deprecated**: These add-ons will no longer receive any updates.`,
},
rating: {
title: "Add-on Security Rating",
@@ -202,14 +202,14 @@ class HassioAddonInfo extends LitElement {
<ha-svg-icon
title="Add-on is running"
class="running"
path=${mdiCircle}
.path=${mdiCircle}
></ha-svg-icon>
`
: html`
<ha-svg-icon
title="Add-on is stopped"
class="stopped"
path=${mdiCircle}
.path=${mdiCircle}
></ha-svg-icon>
`}
`
@@ -283,7 +283,7 @@ class HassioAddonInfo extends LitElement {
label="host"
description=""
>
<ha-svg-icon path=${mdiNetwork}></ha-svg-icon>
<ha-svg-icon .path=${mdiNetwork}></ha-svg-icon>
</ha-label-badge>
`
: ""}
@@ -295,7 +295,7 @@ class HassioAddonInfo extends LitElement {
label="hardware"
description=""
>
<ha-svg-icon path=${mdiChip}></ha-svg-icon>
<ha-svg-icon .path=${mdiChip}></ha-svg-icon>
</ha-label-badge>
`
: ""}
@@ -307,7 +307,7 @@ class HassioAddonInfo extends LitElement {
label="hass"
description=""
>
<ha-svg-icon path=${mdiHomeAssistant}></ha-svg-icon>
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</ha-label-badge>
`
: ""}
@@ -319,7 +319,7 @@ class HassioAddonInfo extends LitElement {
label="hassio"
.description=${this.addon.hassio_role}
>
<ha-svg-icon path=${mdiHomeAssistant}></ha-svg-icon>
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</ha-label-badge>
`
: ""}
@@ -331,7 +331,7 @@ class HassioAddonInfo extends LitElement {
label="docker"
description=""
>
<ha-svg-icon path=${mdiDocker}></ha-svg-icon>
<ha-svg-icon .path=${mdiDocker}></ha-svg-icon>
</ha-label-badge>
`
: ""}
@@ -343,7 +343,7 @@ class HassioAddonInfo extends LitElement {
label="host pid"
description=""
>
<ha-svg-icon path=${mdiPound}></ha-svg-icon>
<ha-svg-icon .path=${mdiPound}></ha-svg-icon>
</ha-label-badge>
`
: ""}
@@ -356,7 +356,7 @@ class HassioAddonInfo extends LitElement {
label="apparmor"
description=""
>
<ha-svg-icon path=${mdiShield}></ha-svg-icon>
<ha-svg-icon .path=${mdiShield}></ha-svg-icon>
</ha-label-badge>
`
: ""}
@@ -368,7 +368,7 @@ class HassioAddonInfo extends LitElement {
label="auth"
description=""
>
<ha-svg-icon path=${mdiKey}></ha-svg-icon>
<ha-svg-icon .path=${mdiKey}></ha-svg-icon>
</ha-label-badge>
`
: ""}
@@ -381,7 +381,7 @@ class HassioAddonInfo extends LitElement {
description=""
>
<ha-svg-icon
path=${mdiCursorDefaultClickOutline}
.path=${mdiCursorDefaultClickOutline}
></ha-svg-icon>
</ha-label-badge>
`
@@ -798,10 +798,10 @@ class HassioAddonInfo extends LitElement {
);
if (!validate.data.valid) {
await showConfirmationDialog(this, {
title: "Failed to start addon - configruation validation faled!",
title: "Failed to start addon - configuration validation failed!",
text: validate.data.message.split(" Got ")[0],
confirm: () => this._openConfiguration(),
confirmText: "Go to configruation",
confirmText: "Go to configuration",
dismissText: "Cancel",
});
button.progress = false;

View File

@@ -39,7 +39,8 @@ import type { HomeAssistant } from "../../../../src/types";
import { HassioNetworkDialogParams } from "./show-dialog-network";
@customElement("dialog-hassio-network")
export class DialogHassioNetwork extends LitElement implements HassDialog {
export class DialogHassioNetwork extends LitElement
implements HassDialog<HassioNetworkDialogParams> {
@property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _prosessing = false;

View File

@@ -39,7 +39,7 @@ class HassioRepositoriesDialog extends LitElement {
@property({ attribute: false })
private _dialogParams?: HassioRepositoryDialogParams;
@query("#repository_input") private _optionInput?: PaperInputElement;
@query("#repository_input", true) private _optionInput?: PaperInputElement;
@internalProperty() private _opened = false;
@@ -91,7 +91,7 @@ class HassioRepositoriesDialog extends LitElement {
title="Remove"
@click=${this._removeRepository}
>
<ha-svg-icon path=${mdiDelete}></ha-svg-icon>
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
</mwc-icon-button>
</paper-item>
`;

View File

@@ -19,7 +19,7 @@ import { HassioSnapshotUploadDialogParams } from "./show-dialog-snapshot-upload"
@customElement("dialog-hassio-snapshot-upload")
export class DialogHassioSnapshotUpload extends LitElement
implements HassDialog {
implements HassDialog<HassioSnapshotUploadDialogParams> {
@property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _params?: HassioSnapshotUploadDialogParams;

View File

@@ -196,7 +196,7 @@ class HassioSnapshotDialog extends LitElement {
@click=${this._downloadClicked}
slot="primaryAction"
>
<ha-svg-icon path=${mdiDownload} class="icon"></ha-svg-icon>
<ha-svg-icon .path=${mdiDownload} class="icon"></ha-svg-icon>
Download Snapshot
</mwc-button>`
: ""}
@@ -205,7 +205,7 @@ class HassioSnapshotDialog extends LitElement {
@click=${this._partialRestoreClicked}
slot="secondaryAction"
>
<ha-svg-icon path=${mdiHistory} class="icon"></ha-svg-icon>
<ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon>
Restore Selected
</mwc-button>
${this._snapshot.type === "full"
@@ -214,7 +214,7 @@ class HassioSnapshotDialog extends LitElement {
@click=${this._fullRestoreClicked}
slot="secondaryAction"
>
<ha-svg-icon path=${mdiHistory} class="icon"></ha-svg-icon>
<ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon>
Wipe &amp; restore
</mwc-button>
`
@@ -224,7 +224,10 @@ class HassioSnapshotDialog extends LitElement {
@click=${this._deleteClicked}
slot="secondaryAction"
>
<ha-svg-icon path=${mdiDelete} class="icon warning"></ha-svg-icon>
<ha-svg-icon
.path=${mdiDelete}
class="icon warning"
></ha-svg-icon>
<span class="warning">Delete Snapshot</span>
</mwc-button>`
: ""}
@@ -440,6 +443,19 @@ class HassioSnapshotDialog extends LitElement {
return;
}
if (window.location.href.includes("ui.nabu.casa")) {
const confirm = await showConfirmationDialog(this, {
title: "Potential slow download",
text:
"Downloading snapshots over the Nabu Casa URL will take some time, it is recomended to use your local URL instead, do you want to continue?",
confirmText: "continue",
dismissText: "cancel",
});
if (!confirm) {
return;
}
}
const name = this._computeName.replace(/[^a-z0-9]+/gi, "_");
const a = document.createElement("a");
a.href = signedPath.path;

View File

@@ -8,7 +8,7 @@ export const supervisorTabs: PageNavigation[] = [
iconPath: mdiViewDashboard,
},
{
name: "Add-on store",
name: "Add-on Store",
path: `/hassio/store`,
iconPath: mdiStore,
},

View File

@@ -57,7 +57,7 @@ class HassioIngressView extends LitElement {
aria-label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
@click=${this._toggleMenu}
>
<ha-svg-icon path=${mdiMenu}></ha-svg-icon>
<ha-svg-icon .path=${mdiMenu}></ha-svg-icon>
</mwc-icon-button>
<div class="main-title">${this._addon.name}</div>
</div>

View File

@@ -117,7 +117,7 @@ class HassioSnapshots extends LitElement {
@action=${this._handleAction}
>
<mwc-icon-button slot="trigger" alt="menu">
<ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon>
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item>
Reload
@@ -131,7 +131,7 @@ class HassioSnapshots extends LitElement {
<div class="content">
<h1>
Create snapshot
Create Snapshot
</h1>
<p class="description">
Snapshots allow you to easily backup and restore all data of your
@@ -219,7 +219,7 @@ class HassioSnapshots extends LitElement {
</ha-card>
</div>
<h1>Available snapshots</h1>
<h1>Available Snapshots</h1>
<div class="card-group">
${this._snapshots === undefined
? undefined

View File

@@ -87,7 +87,7 @@ class HassioHostInfo extends LitElement {
${this.hostInfo.features.includes("network")
? html` <ha-settings-row>
<span slot="heading">
IP address
IP Address
</span>
<span slot="description">
${primaryIpAddress}
@@ -103,7 +103,7 @@ class HassioHostInfo extends LitElement {
<ha-settings-row>
<span slot="heading">
Operating system
Operating System
</span>
<span slot="description">
${this.hostInfo.operating_system}
@@ -221,7 +221,7 @@ class HassioHostInfo extends LitElement {
});
} catch (err) {
showAlertDialog(this, {
title: "Failed to get Hardware list",
title: "Failed to get hardware list",
text: extractApiErrorMessage(err),
});
}
@@ -324,7 +324,7 @@ class HassioHostInfo extends LitElement {
private async _changeHostnameClicked(): Promise<void> {
const curHostname: string = this.hostInfo.hostname;
const hostname = await showPromptDialog(this, {
title: "Change hostname",
title: "Change Hostname",
inputLabel: "Please enter a new hostname:",
inputType: "string",
defaultValue: curHostname,

View File

@@ -51,7 +51,7 @@ class HassioSupervisorInfo extends LitElement {
</ha-settings-row>
<ha-settings-row>
<span slot="heading">
Newest version
Newest Version
</span>
<span slot="description">
${this.supervisorInfo.version_latest}
@@ -98,7 +98,7 @@ class HassioSupervisorInfo extends LitElement {
${this.supervisorInfo?.supported
? html` <ha-settings-row three-line>
<span slot="heading">
Share diagnostics
Share Diagnostics
</span>
<div slot="description" class="diagnostics-description">
Share crash reports and diagnostic information.
@@ -135,7 +135,7 @@ class HassioSupervisorInfo extends LitElement {
<div class="card-actions">
<ha-progress-button
@click=${this._supervisorReload}
title="Reload parts of the supervisor."
title="Reload parts of the supervisor"
>
Reload
</ha-progress-button>
@@ -212,7 +212,7 @@ class HassioSupervisorInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Update supervisor",
title: "Update Supervisor",
text: `Are you sure you want to update supervisor to version ${this.supervisorInfo.version_latest}?`,
confirmText: "update",
dismissText: "cancel",

View File

@@ -76,7 +76,7 @@ class HassioSupervisorLog extends LitElement {
${this.hass.userData?.showAdvanced
? html`
<paper-dropdown-menu
label="Log provider"
label="Log Provider"
@iron-select=${this._setLogProvider}
>
<paper-listbox

View File

@@ -25,6 +25,7 @@ import {
getValueInPercentage,
roundWithOneDecimal,
} from "../../../src/util/calculate";
import { bytesToString } from "../../../src/util/bytes-to-string";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-system-metrics")
@@ -38,35 +39,47 @@ class HassioSystemMetrics extends LitElement {
@internalProperty() private _coreMetrics?: HassioStats;
protected render(): TemplateResult | void {
const usedSpace = this._getUsedSpace(this.hostInfo);
const metrics = [
{
description: "Core CPU usage",
description: "Core CPU Usage",
value: this._coreMetrics?.cpu_percent,
},
{
description: "Core RAM usage",
description: "Core RAM Usage",
value: this._coreMetrics?.memory_percent,
tooltip: `${bytesToString(
this._coreMetrics?.memory_usage
)}/${bytesToString(this._coreMetrics?.memory_limit)}`,
},
{
description: "Supervisor CPU usage",
description: "Supervisor CPU Usage",
value: this._supervisorMetrics?.cpu_percent,
},
{
description: "Supervisor RAM usage",
description: "Supervisor RAM Usage",
value: this._supervisorMetrics?.memory_percent,
tooltip: `${bytesToString(
this._supervisorMetrics?.memory_usage
)}/${bytesToString(this._supervisorMetrics?.memory_limit)}`,
},
{
description: "Used space",
value: usedSpace,
description: "Used Space",
value: this._getUsedSpace(this.hostInfo),
tooltip: `${
this.hostInfo.disk_used
} GB/${this.hostInfo.disk_total} GB`,
},
];
return html`
<ha-card header="System metrics">
<ha-card header="System Metrics">
<div class="card-content">
${metrics.map((metric) =>
this._renderMetric(metric.description, metric.value ?? 0)
this._renderMetric(
metric.description,
metric.value ?? 0,
metric.tooltip
)
)}
</div>
</ha-card>
@@ -77,13 +90,17 @@ class HassioSystemMetrics extends LitElement {
this._loadData();
}
private _renderMetric(description: string, value: number): TemplateResult {
private _renderMetric(
description: string,
value: number,
tooltip?: string
): TemplateResult {
const roundedValue = roundWithOneDecimal(value);
return html`<ha-settings-row>
<span slot="heading">
${description}
</span>
<div slot="description">
<div slot="description" title="${tooltip ?? ""}">
<span class="value">
${roundedValue}%
</span>
@@ -155,6 +172,7 @@ class HassioSystemMetrics extends LitElement {
}
.value {
width: 42px;
padding-right: 4px;
}
`,
];

View File

@@ -28,22 +28,22 @@
"@fullcalendar/daygrid": "5.1.0",
"@fullcalendar/interaction": "5.1.0",
"@fullcalendar/list": "5.1.0",
"@material/chips": "=8.0.0-canary.096a7a066.0",
"@material/circular-progress": "=8.0.0-canary.a78ceb112.0",
"@material/mwc-button": "^0.18.0",
"@material/mwc-checkbox": "^0.18.0",
"@material/mwc-dialog": "^0.18.0",
"@material/mwc-fab": "^0.18.0",
"@material/mwc-formfield": "^0.18.0",
"@material/mwc-icon-button": "^0.18.0",
"@material/mwc-list": "^0.18.0",
"@material/mwc-menu": "^0.18.0",
"@material/mwc-radio": "^0.18.0",
"@material/mwc-ripple": "^0.18.0",
"@material/mwc-switch": "^0.18.0",
"@material/mwc-tab": "^0.18.0",
"@material/mwc-tab-bar": "^0.18.0",
"@material/top-app-bar": "=8.0.0-canary.096a7a066.0",
"@material/chips": "=8.0.0-canary.774dcfc8e.0",
"@material/mwc-button": "^0.19.0",
"@material/mwc-checkbox": "^0.19.0",
"@material/mwc-circular-progress": "^0.19.0",
"@material/mwc-dialog": "^0.19.0",
"@material/mwc-fab": "^0.19.0",
"@material/mwc-formfield": "^0.19.0",
"@material/mwc-icon-button": "^0.19.0",
"@material/mwc-list": "^0.19.0",
"@material/mwc-menu": "^0.19.0",
"@material/mwc-radio": "^0.19.0",
"@material/mwc-ripple": "^0.19.0",
"@material/mwc-switch": "^0.19.0",
"@material/mwc-tab": "^0.19.0",
"@material/mwc-tab-bar": "^0.19.0",
"@material/top-app-bar": "=8.0.0-canary.774dcfc8e.0",
"@mdi/js": "5.6.55",
"@mdi/svg": "5.6.55",
"@polymer/app-layout": "^3.0.2",
@@ -88,11 +88,11 @@
"chartjs-chart-timeline": "^0.3.0",
"codemirror": "^5.49.0",
"comlink": "^4.3.0",
"core-js": "^3.6.5",
"cpx": "^1.5.0",
"cropperjs": "^1.5.7",
"deep-clone-simple": "^1.1.1",
"deep-freeze": "^0.0.1",
"es6-object-assign": "^1.1.0",
"fecha": "^4.2.0",
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
@@ -103,8 +103,8 @@
"js-yaml": "^3.13.1",
"leaflet": "^1.4.0",
"leaflet-draw": "^1.0.4",
"lit-element": "^2.3.1",
"lit-html": "^1.2.1",
"lit-element": "^2.4.0",
"lit-html": "^1.3.0",
"lit-virtualizer": "^0.4.2",
"marked": "^1.1.1",
"mdn-polyfills": "^5.16.0",
@@ -112,6 +112,7 @@
"node-vibrant": "^3.1.5",
"proxy-polyfill": "^0.3.1",
"punycode": "^2.1.1",
"qrcode": "^1.4.4",
"regenerator-runtime": "^0.13.2",
"resize-observer-polyfill": "^1.5.1",
"roboto-fontface": "^0.10.0",
@@ -128,16 +129,17 @@
"xss": "^1.0.6"
},
"devDependencies": {
"@babel/core": "^7.9.0",
"@babel/plugin-external-helpers": "^7.8.3",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-decorators": "^7.8.3",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
"@babel/plugin-proposal-object-rest-spread": "^7.9.5",
"@babel/plugin-proposal-optional-chaining": "^7.9.0",
"@babel/core": "^7.11.6",
"@babel/plugin-external-helpers": "^7.10.4",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-decorators": "^7.10.5",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4",
"@babel/plugin-proposal-object-rest-spread": "^7.11.0",
"@babel/plugin-proposal-optional-chaining": "^7.11.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/preset-env": "^7.9.5",
"@babel/preset-typescript": "^7.9.0",
"@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/preset-env": "^7.11.5",
"@babel/preset-typescript": "^7.10.4",
"@rollup/plugin-commonjs": "^11.1.0",
"@rollup/plugin-json": "^4.0.3",
"@rollup/plugin-node-resolve": "^7.1.3",
@@ -154,8 +156,8 @@
"@types/mocha": "^7.0.2",
"@types/resize-observer-browser": "^0.1.3",
"@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^2.28.0",
"@typescript-eslint/parser": "^2.28.0",
"@typescript-eslint/eslint-plugin": "^4.4.0",
"@typescript-eslint/parser": "^4.4.0",
"babel-loader": "^8.1.0",
"chai": "^4.2.0",
"del": "^4.0.0",
@@ -180,7 +182,7 @@
"html-minifier": "^4.0.0",
"husky": "^1.3.1",
"lint-staged": "^8.1.5",
"lit-analyzer": "^1.2.0",
"lit-analyzer": "^1.2.1",
"lodash.template": "^4.5.0",
"magic-string": "^0.25.7",
"map-stream": "^0.0.7",
@@ -201,29 +203,24 @@
"source-map-url": "^0.4.0",
"systemjs": "^6.3.2",
"terser-webpack-plugin": "^3.0.6",
"ts-lit-plugin": "^1.2.0",
"ts-lit-plugin": "^1.2.1",
"ts-mocha": "^7.0.0",
"typescript": "^3.8.3",
"typescript": "^4.0.3",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"webpack": "^4.40.2",
"webpack-cli": "^3.3.9",
"webpack": "5.0.0-rc.3",
"webpack-cli": "4.0.0-rc.0",
"webpack-dev-server": "^3.10.3",
"webpack-manifest-plugin": "^2.0.4",
"workbox-build": "^5.1.3",
"worker-plugin": "^4.0.3"
"webpack-manifest-plugin": "3.0.0-rc.0",
"workbox-build": "^5.1.3"
},
"_comment": "Polymer fixed to 3.1 because 3.2 throws on logbook page",
"_comment_2": "Fix in https://github.com/Polymer/polymer/pull/5569",
"resolutions": {
"@webcomponents/webcomponentsjs": "^2.2.10",
"@polymer/polymer": "3.1.0",
"lit-html": "1.2.1",
"lit-element": "2.3.1",
"@material/animation": "8.0.0-canary.096a7a066.0",
"@material/base": "8.0.0-canary.096a7a066.0",
"@material/feature-targeting": "8.0.0-canary.096a7a066.0",
"@material/theme": "8.0.0-canary.096a7a066.0"
"lit-html": "1.3.0",
"lit-element": "2.4.0"
},
"main": "src/home-assistant.js",
"husky": {

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20200930.0",
version="20201001.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",

View File

@@ -200,7 +200,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
private _redirect(authCode: string) {
// OAuth 2: 3.1.2 we need to retain query component of a redirect URI
let url = this.redirectUri!!;
let url = this.redirectUri!;
if (!url.includes("?")) {
url += "?";
} else if (!url.endsWith("&")) {

View File

@@ -38,13 +38,11 @@ export default function relativeTime(
roundedDelta = Math.round(delta);
}
const timeDesc = localize(
`ui.components.relative_time.duration.${unit}`,
return localize(
options.includeTense === false
? `ui.components.relative_time.duration.${unit}`
: `ui.components.relative_time.${tense}_duration.${unit}`,
"count",
roundedDelta
);
return options.includeTense === false
? timeDesc
: localize(`ui.components.relative_time.${tense}`, "time", timeDesc);
}

View File

@@ -18,7 +18,7 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
case "garage_door":
return is_off ? "hass:garage" : "hass:garage-open";
case "power":
return is_off ? "hass:power-off" : "hass:power-on";
return is_off ? "hass:power-plug-off" : "hass:power-plug";
case "gas":
case "problem":
case "safety":

View File

@@ -52,7 +52,7 @@ class SearchInput extends LitElement {
.noLabelFloat=${this.noLabelFloat}
>
<ha-svg-icon
path=${mdiMagnify}
.path=${mdiMagnify}
slot="prefix"
class="prefix"
></ha-svg-icon>
@@ -65,7 +65,7 @@ class SearchInput extends LitElement {
alt="Clear"
title="Clear"
>
<ha-svg-icon path=${mdiClose}></ha-svg-icon>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
`}
</paper-input>

View File

@@ -0,0 +1,29 @@
/**
* Determine whether a sequence of letters exists in another string,
* in that order, allowing for skipping. Ex: "chdr" exists in "chandelier")
*
* filter => sequence of letters
* word => Word to check for sequence
*
* return true if word contains sequence. Otherwise false.
*/
export const fuzzySequentialMatch = (filter: string, word: string) => {
if (filter === "") {
return true;
}
for (let i = 0; i <= filter.length; i++) {
const pos = word.indexOf(filter[0]);
if (pos < 0) {
return false;
}
const newWord = word.substring(pos + 1);
const newFilter = filter.substring(1);
return fuzzySequentialMatch(newFilter, newWord);
}
return true;
};

View File

@@ -5,7 +5,7 @@
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
// eslint-disable-next-line: ban-types
export const debounce = <T extends Function>(
export const debounce = <T extends (...args) => unknown>(
func: T,
wait,
immediate = false

View File

@@ -2,7 +2,10 @@ import { Connection, UnsubscribeFunc } from "home-assistant-js-websocket";
export const subscribeOne = async <T>(
conn: Connection,
subscribe: (conn: Connection, onChange: (items: T) => void) => UnsubscribeFunc
subscribe: (
conn2: Connection,
onChange: (items: T) => void
) => UnsubscribeFunc
) =>
new Promise<T>((resolve) => {
const unsub = subscribe(conn, (items) => {

View File

@@ -5,7 +5,7 @@
// as much as it can, without ever going more than once per `wait` duration;
// but if you'd like to disable the execution on the leading edge, pass
// `false for leading`. To disable execution on the trailing edge, ditto.
export const throttle = <T extends Function>(
export const throttle = <T extends (...args) => unknown>(
func: T,
wait: number,
leading = true,

View File

@@ -21,7 +21,7 @@ class HaProgressButton extends LitElement {
@property({ type: Boolean }) public raised = false;
@query("mwc-button") private _button?: Button;
@query("mwc-button", true) private _button?: Button;
public render(): TemplateResult {
return html`

View File

@@ -73,13 +73,17 @@ export interface DataTableColumnData extends DataTableSortColumnData {
hidden?: boolean;
}
type ClonedDataTableColumnData = Omit<DataTableColumnData, "title"> & {
title?: string;
};
export interface DataTableRowData {
[key: string]: any;
selectable?: boolean;
}
export interface SortableColumnContainer {
[key: string]: DataTableSortColumnData;
[key: string]: ClonedDataTableColumnData;
}
@customElement("ha-data-table")
@@ -101,6 +105,9 @@ export class HaDataTable extends LitElement {
@property({ type: String }) public searchLabel?: string;
@property({ type: Boolean, attribute: "no-label-float" })
public noLabelFloat? = false;
@property({ type: String }) public filter = "";
@internalProperty() private _filterable = false;
@@ -113,9 +120,9 @@ export class HaDataTable extends LitElement {
@internalProperty() private _filteredData: DataTableRowData[] = [];
@query("slot[name='header']") private _header!: HTMLSlotElement;
@internalProperty() private _headerHeight = 0;
@query(".mdc-data-table__table") private _table!: HTMLDivElement;
@query("slot[name='header']") private _header!: HTMLSlotElement;
private _checkableRowsCount?: number;
@@ -166,11 +173,13 @@ export class HaDataTable extends LitElement {
}
const clonedColumns: DataTableColumnContainer = deepClone(this.columns);
Object.values(clonedColumns).forEach((column: DataTableColumnData) => {
delete column.title;
delete column.type;
delete column.template;
});
Object.values(clonedColumns).forEach(
(column: ClonedDataTableColumnData) => {
delete column.title;
delete column.type;
delete column.template;
}
);
this._sortColumns = clonedColumns;
}
@@ -206,6 +215,7 @@ export class HaDataTable extends LitElement {
<search-input
@value-changed=${this._handleSearchChange}
.label=${this.searchLabel}
.noLabelFloat=${this.noLabelFloat}
></search-input>
</div>
`
@@ -220,7 +230,7 @@ export class HaDataTable extends LitElement {
style=${styleMap({
height: this.autoHeight
? `${(this._filteredData.length || 1) * 53 + 57}px`
: `calc(100% - ${this._header?.clientHeight}px)`,
: `calc(100% - ${this._headerHeight}px)`,
})}
>
<div class="mdc-data-table__header-row" role="row">
@@ -523,7 +533,7 @@ export class HaDataTable extends LitElement {
return;
}
await this.updateComplete;
this._table.style.height = `calc(100% - ${this._header.clientHeight}px)`;
this._headerHeight = this._header.clientHeight;
}
@eventOptions({ passive: true })

View File

@@ -16,7 +16,7 @@ export const filterData = async (
filter: FilterDataParamTypes[2]
): Promise<ReturnType<FilterDataType>> => {
if (!worker) {
worker = wrap(new Worker("./sort_filter_worker", { type: "module" }));
worker = wrap(new Worker(new URL("./sort_filter_worker", import.meta.url)));
}
return await worker.filterData(data, columns, filter);
@@ -29,7 +29,7 @@ export const sortData = async (
sortColumn: SortDataParamTypes[3]
): Promise<ReturnType<SortDataType>> => {
if (!worker) {
worker = wrap(new Worker("./sort_filter_worker", { type: "module" }));
worker = wrap(new Worker(new URL("./sort_filter_worker", import.meta.url)));
}
return await worker.sortData(data, columns, direction, sortColumn);

View File

@@ -1,7 +1,7 @@
// @ts-nocheck
import Vue from "vue";
import wrap from "@vue/web-component-wrapper";
import DateRangePicker from "vue2-daterange-picker";
// @ts-ignore
import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css";
import { fireEvent } from "../common/dom/fire_event";
import { Constructor } from "../types";
@@ -35,7 +35,6 @@ const Component = Vue.extend({
},
},
render(createElement) {
// @ts-ignore
return createElement(DateRangePicker, {
props: {
"time-picker": true,
@@ -52,7 +51,6 @@ const Component = Vue.extend({
endDate: this.endDate,
},
callback: (value) => {
// @ts-ignore
fireEvent(this.$el as HTMLElement, "change", value);
},
expression: "dateRange",

View File

@@ -71,14 +71,24 @@ class HaChartBase extends mixinBehaviors(
margin: 5px 0 0 0;
width: 100%;
}
.chartTooltip ul {
margin: 0 3px;
}
.chartTooltip li {
display: block;
white-space: pre-line;
}
.chartTooltip li::first-line {
line-height: 0;
}
.chartTooltip .title {
text-align: center;
font-weight: 500;
}
.chartTooltip .beforeBody {
text-align: center;
font-weight: 300;
}
.chartLegend li {
display: inline-block;
padding: 0 6px;
@@ -133,6 +143,9 @@ class HaChartBase extends mixinBehaviors(
style$="opacity:[[tooltip.opacity]]; top:[[tooltip.top]]; left:[[tooltip.left]]; padding:[[tooltip.yPadding]]px [[tooltip.xPadding]]px"
>
<div class="title">[[tooltip.title]]</div>
<template is="dom-if" if="[[tooltip.beforeBody]]">
<div class="beforeBody">[[tooltip.beforeBody]]</div>
</template>
<div>
<ul>
<template is="dom-repeat" items="[[tooltip.lines]]">
@@ -264,6 +277,10 @@ class HaChartBase extends mixinBehaviors(
const title = tooltip.title ? tooltip.title[0] || "" : "";
this.set(["tooltip", "title"], title);
if (tooltip.beforeBody) {
this.set(["tooltip", "beforeBody"], tooltip.beforeBody.join("\n"));
}
const bodyLines = tooltip.body.map((n) => n.lines);
// Set Text

View File

@@ -55,7 +55,7 @@ class HaEntityAttributePicker extends LitElement {
@property({ type: Boolean }) private _opened = false;
@query("vaadin-combo-box-light") private _comboBox!: HTMLElement;
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
protected shouldUpdate(changedProps: PropertyValues) {
return !(!changedProps.has("_opened") && this._opened);

View File

@@ -97,7 +97,7 @@ export class HaEntityPicker extends LitElement {
@property({ type: Boolean }) private _opened = false;
@query("vaadin-combo-box-light") private _comboBox!: HTMLElement;
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
private _initedStates = false;
@@ -215,6 +215,7 @@ export class HaEntityPicker extends LitElement {
slot="suffix"
class="clear-button"
icon="hass:close"
tabindex="-1"
@click=${this._clearValue}
no-ripple
>
@@ -230,6 +231,7 @@ export class HaEntityPicker extends LitElement {
slot="suffix"
class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
tabindex="-1"
>
Toggle
</ha-icon-button>

View File

@@ -110,7 +110,9 @@ export class HaStateLabelBadge extends LitElement {
return null;
case "sensor":
default:
return state.state === UNKNOWN
return state.attributes.device_class === "moon__phase"
? null
: state.state === UNKNOWN
? "-"
: state.attributes.unit_of_measurement
? state.state
@@ -162,7 +164,9 @@ export class HaStateLabelBadge extends LitElement {
? "hass:timer-outline"
: "hass:timer-off-outline";
default:
return null;
return state?.attributes.device_class === "moon__phase"
? stateIcon(state)
: null;
}
}

View File

@@ -33,7 +33,9 @@ class HaAttributes extends LitElement {
).map(
(attribute) => html`
<div class="data-entry">
<div class="key">${attribute.replace(/_/g, " ")}</div>
<div class="key">
${attribute.replace(/_/g, " ").replace("id", "ID")}
</div>
<div class="value">
${this.formatAttribute(attribute)}
</div>
@@ -62,6 +64,9 @@ class HaAttributes extends LitElement {
max-width: 200px;
overflow-wrap: break-word;
}
.key:first-letter {
text-transform: capitalize;
}
.attribution {
color: var(--secondary-text-color);
text-align: right;

View File

@@ -34,8 +34,8 @@ export class HaBar extends LitElement {
return svg`
<svg>
<g>
<rect></rect>
<rect width="${valuePrecentage}%"></rect>
<rect/>
<rect width="${valuePrecentage}%"/>
</g>
</svg>
`;
@@ -43,6 +43,9 @@ export class HaBar extends LitElement {
static get styles(): CSSResult {
return css`
rect {
height: 100%;
}
rect:first-child {
width: 100%;
fill: var(--ha-bar-background-color, var(--secondary-background-color));

View File

@@ -23,7 +23,7 @@ export class HaButtonMenu extends LitElement {
@property({ type: Boolean }) public disabled = false;
@query("mwc-menu") private _menu?: Menu;
@query("mwc-menu", true) private _menu?: Menu;
public get items() {
return this._menu?.items;
@@ -62,6 +62,9 @@ export class HaButtonMenu extends LitElement {
display: inline-block;
position: relative;
}
::slotted([disabled]) {
color: var(--disabled-text-color);
}
`;
}
}

View File

@@ -1,20 +1,9 @@
// @ts-ignore
import progressStyles from "@material/circular-progress/dist/mdc.circular-progress.min.css";
import {
css,
customElement,
html,
LitElement,
property,
svg,
SVGTemplateResult,
TemplateResult,
unsafeCSS,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { customElement, property } from "lit-element";
import { CircularProgress } from "@material/mwc-circular-progress";
@customElement("ha-circular-progress")
export class HaCircularProgress extends LitElement {
// @ts-ignore
export class HaCircularProgress extends CircularProgress {
@property({ type: Boolean })
public active = false;
@@ -24,65 +13,31 @@ export class HaCircularProgress extends LitElement {
@property()
public size: "small" | "medium" | "large" = "medium";
protected render(): TemplateResult {
let indeterminatePart: SVGTemplateResult;
if (this.size === "small") {
indeterminatePart = svg`
<svg class="mdc-circular-progress__indeterminate-circle-graphic" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="8.75" stroke-dasharray="54.978" stroke-dashoffset="27.489"/>
</svg>`;
} else if (this.size === "large") {
indeterminatePart = svg`
<svg class="mdc-circular-progress__indeterminate-circle-graphic" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<circle cx="24" cy="24" r="18" stroke-dasharray="113.097" stroke-dashoffset="56.549"/>
</svg>`;
} else {
// medium
indeterminatePart = svg`
<svg class="mdc-circular-progress__indeterminate-circle-graphic" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<circle cx="16" cy="16" r="12.5" stroke-dasharray="78.54" stroke-dashoffset="39.27"/>
</svg>`;
}
// ignoring prettier as it will introduce unwanted whitespace
// We have not implemented the determinate support of mdc circular progress.
// prettier-ignore
return html`
<div
class="mdc-circular-progress ${classMap({
"mdc-circular-progress--indeterminate": this.active,
[`mdc-circular-progress--${this.size}`]: true,
})}"
role="progressbar"
aria-label=${this.alt}
aria-valuemin="0"
aria-valuemax="1"
>
<div class="mdc-circular-progress__indeterminate-container">
<div class="mdc-circular-progress__spinner-layer">
<div class="mdc-circular-progress__circle-clipper mdc-circular-progress__circle-left">
${indeterminatePart}
</div><div class="mdc-circular-progress__gap-patch">
${indeterminatePart}
</div><div class="mdc-circular-progress__circle-clipper mdc-circular-progress__circle-right">
${indeterminatePart}
</div>
</div>
</div>
</div>
`;
// @ts-ignore
public set density(_) {
// just a dummy
}
static get styles() {
return [
unsafeCSS(progressStyles),
css`
:host {
text-align: initial;
}
`,
];
public get density() {
switch (this.size) {
case "small":
return -5;
case "medium":
return 0;
case "large":
return 5;
default:
return 0;
}
}
// @ts-ignore
public set indeterminate(_) {
// just a dummy
}
public get indeterminate() {
return this.active;
}
}

View File

@@ -60,7 +60,7 @@ export class HaDateRangePicker extends LitElement {
?ranges=${this.ranges !== undefined}
>
<div slot="input" class="date-range-inputs">
<ha-svg-icon path=${mdiCalendar}></ha-svg-icon>
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
<paper-input
.value=${formatDateTime(this.startDate, this.hass.language)}
.label=${this.hass.localize(

View File

@@ -17,7 +17,7 @@ export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
class="header_button"
dir=${computeRTLDirection(hass)}
>
<ha-svg-icon path=${mdiClose}></ha-svg-icon>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
`;

View File

@@ -27,7 +27,7 @@ export class HaFormBoolean extends LitElement implements HaFormElement {
@property() public suffix!: string;
@query("paper-checkbox") private _input?: HTMLElement;
@query("paper-checkbox", true) private _input?: HTMLElement;
public focus() {
if (this._input) {

View File

@@ -21,7 +21,7 @@ export class HaFormFloat extends LitElement implements HaFormElement {
@property() public suffix!: string;
@query("paper-input") private _input?: HTMLElement;
@query("paper-input", true) private _input?: HTMLElement;
public focus() {
if (this._input) {

View File

@@ -35,7 +35,7 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
@internalProperty() private _init = false;
@query("paper-menu-button") private _input?: HTMLElement;
@query("paper-menu-button", true) private _input?: HTMLElement;
public focus(): void {
if (this._input) {

View File

@@ -20,7 +20,7 @@ export class HaFormTimePeriod extends LitElement implements HaFormElement {
@property() public suffix!: string;
@query("paper-time-input") private _input?: HTMLElement;
@query("paper-time-input", true) private _input?: HTMLElement;
public focus() {
if (this._input) {

View File

@@ -24,7 +24,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
@property() public suffix!: string;
@query("ha-paper-dropdown-menu") private _input?: HTMLElement;
@query("ha-paper-dropdown-menu", true) private _input?: HTMLElement;
public focus() {
if (this._input) {

View File

@@ -55,6 +55,7 @@ export class HaFormString extends LitElement implements HaFormElement {
id="iconButton"
title="Click to toggle between masked and clear password"
@click=${this._toggleUnmaskedPassword}
tabindex="-1"
>
</ha-icon-button>
</paper-input>

View File

@@ -38,7 +38,7 @@ class HaHLSPlayer extends LitElement {
@property({ type: Boolean, attribute: "allow-exoplayer" })
public allowExoPlayer = false;
@query("video") private _videoEl!: HTMLVideoElement;
@query("video", true) private _videoEl!: HTMLVideoElement;
@internalProperty() private _attached = false;

View File

@@ -62,7 +62,7 @@ class HaMenuButton extends LitElement {
aria-label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
@click=${this._toggleMenu}
>
<ha-svg-icon path=${mdiMenu}></ha-svg-icon>
<ha-svg-icon .path=${mdiMenu}></ha-svg-icon>
</mwc-icon-button>
${hasNotifications ? html` <div class="dot"></div> ` : ""}
`;

View File

@@ -20,7 +20,7 @@ declare global {
}
}
const isEmpty = (obj: object): boolean => {
const isEmpty = (obj: Record<string, unknown>): boolean => {
if (typeof obj !== "object") {
return false;
}
@@ -44,7 +44,7 @@ export class HaYamlEditor extends LitElement {
@internalProperty() private _yaml = "";
@query("ha-code-editor") private _editor?: HaCodeEditor;
@query("ha-code-editor", true) private _editor?: HaCodeEditor;
public setValue(value): void {
try {
@@ -105,6 +105,10 @@ export class HaYamlEditor extends LitElement {
fireEvent(this, "value-changed", { value: parsed, isValid } as any);
}
get yaml() {
return this._editor?.value;
}
}
declare global {

View File

@@ -61,8 +61,8 @@ class LocationEditor extends LitElement {
if (!this._leafletMap || !this.location) {
return;
}
if ((this._locationMarker as Circle).getBounds) {
this._leafletMap.fitBounds((this._locationMarker as Circle).getBounds());
if (this._locationMarker && "getBounds" in this._locationMarker) {
this._leafletMap.fitBounds(this._locationMarker.getBounds());
} else {
this._leafletMap.setView(this.location, this.fitZoom);
}

View File

@@ -90,8 +90,8 @@ export class HaLocationsEditor extends LitElement {
if (!marker) {
return;
}
if ((marker as Circle).getBounds) {
this._leafletMap.fitBounds((marker as Circle).getBounds());
if ("getBounds" in marker) {
this._leafletMap.fitBounds(marker.getBounds());
(marker as Circle).bringToFront();
} else {
const circle = this._circles[id];
@@ -296,8 +296,8 @@ export class HaLocationsEditor extends LitElement {
// @ts-ignore
(ev: MouseEvent) => this._markerClicked(ev)
)
.addTo(this._leafletMap);
marker.id = location.id;
.addTo(this._leafletMap!);
(marker as any).id = location.id;
this._locationMarkers![location.id] = marker;
}

View File

@@ -159,7 +159,7 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
if (prevState !== null) {
dataRow.push([prevLastChanged, endTime, locState, prevState]);
}
datasets.push({ data: dataRow });
datasets.push({ data: dataRow, entity_id: stateInfo.entity_id });
labels.push(entityDisplay);
});
@@ -173,12 +173,22 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
return [state, start, end];
};
const formatTooltipBeforeBody = (item, data) => {
if (!this.hass.userData || !this.hass.userData.showAdvanced || !item[0]) {
return "";
}
// Extract the entity ID from the dataset.
const values = data.datasets[item[0].datasetIndex];
return values.entity_id || "";
};
const chartOptions = {
type: "timeline",
options: {
tooltips: {
callbacks: {
label: formatTooltipLabel,
beforeBody: formatTooltipBeforeBody,
},
},
scales: {

View File

@@ -217,12 +217,12 @@ export const subscribeTrigger = (
hass: HomeAssistant,
onChange: (result: {
variables: {
trigger: {};
trigger: Record<string, unknown>;
};
context: Context;
}) => void,
trigger: Trigger | Trigger[],
variables?: {}
variables?: Record<string, unknown>
) =>
hass.connection.subscribeMessage(onChange, {
type: "subscribe_trigger",
@@ -233,7 +233,7 @@ export const subscribeTrigger = (
export const testCondition = (
hass: HomeAssistant,
condition: Condition | Condition[],
variables?: {}
variables?: Record<string, unknown>
) =>
hass.callWS<{ result: boolean }>({
type: "test_condition",

View File

@@ -10,7 +10,6 @@ import {
} from "./history";
export interface CacheConfig {
refresh: number;
cacheKey: string;
hoursToShow: number;
}

View File

@@ -17,12 +17,12 @@ interface OptimisticCollection<T> extends Collection<T> {
*/
export const getOptimisticCollection = <StateType>(
saveCollection: (conn: Connection, data: StateType) => Promise<unknown>,
saveCollection: (conn2: Connection, data: StateType) => Promise<unknown>,
conn: Connection,
key: string,
fetchCollection: (conn: Connection) => Promise<StateType>,
fetchCollection: (conn2: Connection) => Promise<StateType>,
subscribeUpdates?: (
conn: Connection,
conn2: Connection,
store: Store<StateType>
) => Promise<UnsubscribeFunc>
): OptimisticCollection<StateType> => {

View File

@@ -15,7 +15,7 @@ export interface EntityRegistryEntry {
export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
unique_id: string;
capabilities: object;
capabilities: Record<string, unknown>;
original_name?: string;
original_icon?: string;
}

View File

@@ -34,9 +34,9 @@ export interface HassioAddonDetails extends HassioAddonInfo {
version_latest: string;
boot: "auto" | "manual";
build: boolean;
options: object;
network: null | object;
network_description: null | object;
options: Record<string, unknown>;
network: null | Record<string, number>;
network_description: null | Record<string, string>;
host_network: boolean;
host_pid: boolean;
host_ipc: boolean;
@@ -96,11 +96,11 @@ export interface HassioAddonRepository {
export interface HassioAddonSetOptionParams {
audio_input?: string | null;
audio_output?: string | null;
options?: object | null;
options?: Record<string, unknown> | null;
boot?: "auto" | "manual";
auto_update?: boolean;
ingress_panel?: boolean;
network?: object | null;
network?: Record<string, unknown> | null;
watchdog?: boolean;
}

View File

@@ -18,7 +18,7 @@ export interface HassioHardwareInfo {
input: string[];
disk: string[];
gpio: string[];
audio: object;
audio: Record<string, unknown>;
}
export const fetchHassioHardwareAudio = async (hass: HomeAssistant) => {

View File

@@ -6,12 +6,14 @@ import { HomeAssistant } from "../types";
import { UNAVAILABLE_STATES } from "./entity";
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
export const CONTINUOUS_DOMAINS = ["proximity", "sensor"];
export interface LogbookEntry {
when: string;
name: string;
message?: string;
entity_id?: string;
icon?: string;
domain: string;
context_user_id?: string;
context_event_type?: string;
@@ -43,11 +45,12 @@ export const getLogbookData = async (
);
for (const entry of logbookData) {
if (entry.state) {
const stateObj = hass!.states[entry.entity_id!];
if (entry.state && stateObj) {
entry.message = getLogbookMessage(
hass,
entry.state,
hass!.states[entry.entity_id!],
stateObj,
computeDomain(entry.entity_id!)
);
}

View File

@@ -9,6 +9,6 @@ export interface CustomPanelConfig {
html_url?: string;
}
export type CustomPanelInfo<T = {}> = PanelInfo<
export type CustomPanelInfo<T = Record<string, unknown>> = PanelInfo<
T & { _panel_custom: CustomPanelConfig }
>;

View File

@@ -105,7 +105,7 @@ export type Action =
export const triggerScript = (
hass: HomeAssistant,
entityId: string,
variables?: {}
variables?: Record<string, unknown>
) => hass.callService("script", computeObjectId(entityId), variables);
export const canExcecute = (state: ScriptEntity) => {

View File

@@ -33,8 +33,8 @@ export const getHassTranslations = async (
category: TranslationCategory,
integration?: string,
config_flow?: boolean
): Promise<{}> => {
const result = await hass.callWS<{ resources: {} }>({
): Promise<Record<string, unknown>> => {
const result = await hass.callWS<{ resources: Record<string, unknown> }>({
type: "frontend/get_translations",
language,
category,
@@ -47,8 +47,8 @@ export const getHassTranslations = async (
export const getHassTranslationsPre109 = async (
hass: HomeAssistant,
language: string
): Promise<{}> => {
const result = await hass.callWS<{ resources: {} }>({
): Promise<Record<string, unknown>> => {
const result = await hass.callWS<{ resources: Record<string, unknown> }>({
type: "frontend/get_translations",
language,
});

View File

@@ -48,7 +48,7 @@ const snowyStates = new Set<string>(["snowy", "snowy-rainy"]);
const lightningStates = new Set<string>(["lightning", "lightning-rainy"]);
export const cardinalDirections = [
const cardinalDirections = [
"N",
"NNE",
"NE",
@@ -77,13 +77,29 @@ const getWindBearingText = (degree: string): string => {
return degree;
};
export const getWindBearing = (bearing: string): string => {
const getWindBearing = (bearing: string): string => {
if (bearing != null) {
return getWindBearingText(bearing);
}
return "";
};
export const getWind = (
hass: HomeAssistant,
speed: string,
bearing: string
): string => {
if (bearing !== null) {
const cardinalDirection = getWindBearing(bearing);
return `${speed} ${getWeatherUnit(hass!, "wind_speed")} (${
hass.localize(
`ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}`
) || cardinalDirection
})`;
}
return `${speed} ${getWeatherUnit(hass!, "wind_speed")}`;
};
export const getWeatherUnit = (
hass: HomeAssistant,
measure: string
@@ -210,7 +226,7 @@ export const weatherSVGStyles = css`
}
`;
export const getWeatherStateSVG = (
const getWeatherStateSVG = (
state: string,
nightTime?: boolean
): SVGTemplateResult => {

View File

@@ -17,7 +17,7 @@ export const subscribeRenderTemplate = (
params: {
template: string;
entity_ids?: string | string[];
variables?: object;
variables?: Record<string, unknown>;
timeout?: number;
}
): Promise<UnsubscribeFunc> => {

View File

@@ -19,7 +19,8 @@ import { HassDialog } from "../make-dialog-manager";
import { HaDomainTogglerDialogParams } from "./show-dialog-domain-toggler";
@customElement("dialog-domain-toggler")
class DomainTogglerDialog extends LitElement implements HassDialog {
class DomainTogglerDialog extends LitElement
implements HassDialog<HaDomainTogglerDialogParams> {
public hass!: HomeAssistant;
@internalProperty() private _params?: HaDomainTogglerDialogParams;

View File

@@ -29,7 +29,7 @@ export class HaImagecropperDialog extends LitElement {
@internalProperty() private _open = false;
@query("img") private _image!: HTMLImageElement;
@query("img", true) private _image!: HTMLImageElement;
private _cropper?: Cropper;

View File

@@ -1,3 +1,4 @@
import "../../../components/ha-svg-icon";
import { HassEntity } from "home-assistant-js-websocket";
import {
css,
@@ -9,44 +10,47 @@ import {
} from "lit-element";
import { html, TemplateResult } from "lit-html";
import { HomeAssistant } from "../../../types";
import "../../../components/ha-icon";
const cardinalDirections = [
"N",
"NNE",
"NE",
"ENE",
"E",
"ESE",
"SE",
"SSE",
"S",
"SSW",
"SW",
"WSW",
"W",
"WNW",
"NW",
"NNW",
"N",
];
import { getWind, getWeatherUnit } from "../../../data/weather";
import {
mdiAlertCircleOutline,
mdiEye,
mdiGauge,
mdiThermometer,
mdiWaterPercent,
mdiWeatherCloudy,
mdiWeatherFog,
mdiWeatherHail,
mdiWeatherLightning,
mdiWeatherLightningRainy,
mdiWeatherNight,
mdiWeatherPartlyCloudy,
mdiWeatherPouring,
mdiWeatherRainy,
mdiWeatherSnowy,
mdiWeatherSnowyRainy,
mdiWeatherSunny,
mdiWeatherWindy,
mdiWeatherWindyVariant,
} from "@mdi/js";
const weatherIcons = {
"clear-night": "hass:weather-night",
cloudy: "hass:weather-cloudy",
exceptional: "hass:alert-circle-outline",
fog: "hass:weather-fog",
hail: "hass:weather-hail",
lightning: "hass:weather-lightning",
"lightning-rainy": "hass:weather-lightning-rainy",
partlycloudy: "hass:weather-partly-cloudy",
pouring: "hass:weather-pouring",
rainy: "hass:weather-rainy",
snowy: "hass:weather-snowy",
"snowy-rainy": "hass:weather-snowy-rainy",
sunny: "hass:weather-sunny",
windy: "hass:weather-windy",
"windy-variant": "hass:weather-windy-variant",
"clear-night": mdiWeatherNight,
cloudy: mdiWeatherCloudy,
exceptional: mdiAlertCircleOutline,
fog: mdiWeatherFog,
hail: mdiWeatherHail,
lightning: mdiWeatherLightning,
"lightning-rainy": mdiWeatherLightningRainy,
partlycloudy: mdiWeatherPartlyCloudy,
pouring: mdiWeatherPouring,
rainy: mdiWeatherRainy,
snowy: mdiWeatherSnowy,
"snowy-rainy": mdiWeatherSnowyRainy,
sunny: mdiWeatherSunny,
windy: mdiWeatherWindy,
"windy-variant": mdiWeatherWindyVariant,
};
@customElement("more-info-weather")
@@ -79,24 +83,25 @@ class MoreInfoWeather extends LitElement {
return html`
<div class="flex">
<ha-icon icon="hass:thermometer"></ha-icon>
<ha-svg-icon .path=${mdiThermometer}></ha-svg-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.temperature")}
</div>
<div>
${this.stateObj.attributes.temperature} ${this.getUnit("temperature")}
${this.stateObj.attributes.temperature}
${getWeatherUnit(this.hass, "temperature")}
</div>
</div>
${this._showValue(this.stateObj.attributes.pressure)
? html`
<div class="flex">
<ha-icon icon="hass:gauge"></ha-icon>
<ha-svg-icon .path=${mdiGauge}></ha-svg-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.air_pressure")}
</div>
<div>
${this.stateObj.attributes.pressure}
${this.getUnit("air_pressure")}
${getWeatherUnit(this.hass, "air_pressure")}
</div>
</div>
`
@@ -104,7 +109,7 @@ class MoreInfoWeather extends LitElement {
${this._showValue(this.stateObj.attributes.humidity)
? html`
<div class="flex">
<ha-icon icon="hass:water-percent"></ha-icon>
<ha-svg-icon .path=${mdiWaterPercent}></ha-svg-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.humidity")}
</div>
@@ -115,12 +120,13 @@ class MoreInfoWeather extends LitElement {
${this._showValue(this.stateObj.attributes.wind_speed)
? html`
<div class="flex">
<ha-icon icon="hass:weather-windy"></ha-icon>
<ha-svg-icon .path=${mdiWeatherWindy}></ha-svg-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.wind_speed")}
</div>
<div>
${this.getWind(
${getWind(
this.hass,
this.stateObj.attributes.wind_speed,
this.stateObj.attributes.wind_bearing
)}
@@ -131,12 +137,13 @@ class MoreInfoWeather extends LitElement {
${this._showValue(this.stateObj.attributes.visibility)
? html`
<div class="flex">
<ha-icon icon="hass:eye"></ha-icon>
<ha-svg-icon .path=${mdiEye}></ha-svg-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.visibility")}
</div>
<div>
${this.stateObj.attributes.visibility} ${this.getUnit("length")}
${this.stateObj.attributes.visibility}
${getWeatherUnit(this.hass, "length")}
</div>
</div>
`
@@ -151,9 +158,9 @@ class MoreInfoWeather extends LitElement {
<div class="flex">
${item.condition
? html`
<ha-icon
.icon="${weatherIcons[item.condition]}"
></ha-icon>
<ha-svg-icon
.path="${weatherIcons[item.condition]}"
></ha-svg-icon>
`
: ""}
${!this._showValue(item.templow)
@@ -169,12 +176,14 @@ class MoreInfoWeather extends LitElement {
${this.computeDate(item.datetime)}
</div>
<div class="templow">
${item.templow} ${this.getUnit("temperature")}
${item.templow}
${getWeatherUnit(this.hass, "temperature")}
</div>
`
: ""}
<div class="temp">
${item.temperature} ${this.getUnit("temperature")}
${item.temperature}
${getWeatherUnit(this.hass, "temperature")}
</div>
</div>
`;
@@ -193,7 +202,7 @@ class MoreInfoWeather extends LitElement {
static get styles(): CSSResult {
return css`
ha-icon {
ha-svg-icon {
color: var(--paper-item-icon-color);
}
.section {
@@ -247,41 +256,6 @@ class MoreInfoWeather extends LitElement {
});
}
private getUnit(measure: string): string {
const lengthUnit = this.hass.config.unit_system.length || "";
switch (measure) {
case "air_pressure":
return lengthUnit === "km" ? "hPa" : "inHg";
case "length":
return lengthUnit;
case "precipitation":
return lengthUnit === "km" ? "mm" : "in";
default:
return this.hass.config.unit_system[measure] || "";
}
}
private windBearingToText(degree: string): string {
const degreenum = parseInt(degree, 10);
if (isFinite(degreenum)) {
// eslint-disable-next-line no-bitwise
return cardinalDirections[(((degreenum + 11.25) / 22.5) | 0) % 16];
}
return degree;
}
private getWind(speed: string, bearing: string) {
if (bearing != null) {
const cardinalDirection = this.windBearingToText(bearing);
return `${speed} ${this.getUnit("length")}/h (${
this.hass.localize(
`ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}`
) || cardinalDirection
})`;
}
return `${speed} ${this.getUnit("length")}/h`;
}
private _showValue(item: string): boolean {
return typeof item !== "undefined" && item !== null;
}

View File

@@ -40,7 +40,14 @@ import "./ha-more-info-logbook";
import "./controls/more-info-default";
const DOMAINS_NO_INFO = ["camera", "configurator"];
/**
* Entity domains that should be editable *if* they have an id present;
* {@see shouldShowEditIcon}.
* */
const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"];
/**
* Entity Domains that should always be editable; {@see shouldShowEditIcon}.
* */
const EDITABLE_DOMAINS = ["script"];
export interface MoreInfoDialogParams {
@@ -73,6 +80,20 @@ export class MoreInfoDialog extends LitElement {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected shouldShowEditIcon(domain, stateObj): boolean {
if (EDITABLE_DOMAINS_WITH_ID.includes(domain) && stateObj.attributes.id) {
return true;
}
if (EDITABLE_DOMAINS.includes(domain)) {
return true;
}
if (domain === "person" && stateObj.attributes.editable !== "false") {
return true;
}
return false;
}
protected updated(changedProperties) {
if (!this.hass || !this._entityId || !changedProperties.has("_entityId")) {
return;
@@ -137,10 +158,7 @@ export class MoreInfoDialog extends LitElement {
</mwc-icon-button>
`
: ""}
${this.hass.user!.is_admin &&
((EDITABLE_DOMAINS_WITH_ID.includes(domain) &&
stateObj.attributes.id) ||
EDITABLE_DOMAINS.includes(domain))
${this.shouldShowEditIcon(domain, stateObj)
? html`
<mwc-icon-button
slot="actionItems"
@@ -283,14 +301,12 @@ export class MoreInfoDialog extends LitElement {
private _gotoEdit() {
const stateObj = this.hass.states[this._entityId!];
const domain = computeDomain(this._entityId!);
navigate(
this,
`/config/${domain}/edit/${
EDITABLE_DOMAINS_WITH_ID.includes(domain)
? stateObj.attributes.id
: stateObj.entity_id
}`
);
let idToPassThroughUrl = stateObj.entity_id;
if (EDITABLE_DOMAINS_WITH_ID.includes(domain) || domain === "person") {
idToPassThroughUrl = stateObj.attributes.id;
}
navigate(this, `/config/${domain}/edit/${idToPassThroughUrl}`);
this.closeDialog();
}

View File

@@ -78,7 +78,6 @@ export class MoreInfoHistory extends LitElement {
this.hass!,
this.entityId,
{
refresh: 60,
cacheKey: `more_info.${this.entityId}`,
hoursToShow: 24,
},

View File

@@ -13,7 +13,11 @@ import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { throttle } from "../../common/util/throttle";
import "../../components/ha-circular-progress";
import "../../components/state-history-charts";
import { getLogbookData, LogbookEntry } from "../../data/logbook";
import {
CONTINUOUS_DOMAINS,
getLogbookData,
LogbookEntry,
} from "../../data/logbook";
import "../../panels/logbook/ha-logbook";
import { haStyle, haStyleScrollbar } from "../../resources/styles";
import { HomeAssistant } from "../../types";
@@ -40,7 +44,12 @@ export class MoreInfoLogbook extends LitElement {
}
const stateObj = this.hass.states[this.entityId];
if (!stateObj) {
if (!stateObj || stateObj.attributes.unit_of_measurement) {
return html``;
}
const domain = computeStateDomain(stateObj);
if (CONTINUOUS_DOMAINS.includes(domain)) {
return html``;
}
@@ -155,9 +164,9 @@ export class MoreInfoLogbook extends LitElement {
overflow: auto;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-logbook {
max-height: unset;
}
ha-logbook {
max-height: unset;
}
}
ha-circular-progress {
display: flex;

View File

@@ -88,6 +88,7 @@ export class HuiNotificationDrawer extends EventsMixin(
notifications: {
type: Array,
computed: "_computeNotifications(open, hass, _notificationsBackend)",
observer: "_notificationsChanged",
},
_notificationsBackend: {
type: Array,
@@ -130,6 +131,17 @@ export class HuiNotificationDrawer extends EventsMixin(
}
}
_notificationsChanged(newNotifications, oldNotifications) {
// automatically close drawer when last notification has been dismissed
if (
this.open &&
oldNotifications.length > 0 &&
!newNotifications.length === 0
) {
this.open = false;
}
}
_computeNotifications(open, hass, notificationsBackend) {
if (!open) {
return [];

View File

@@ -0,0 +1,225 @@
import "../../components/ha-header-bar";
import "@polymer/paper-input/paper-input";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-list/mwc-list";
import {
css,
customElement,
html,
internalProperty,
LitElement,
property,
} from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-dialog";
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant, ServiceCallRequest } from "../../types";
import { PolymerChangedEvent } from "../../polymer-types";
import { fuzzySequentialMatch } from "../../common/string/sequence_matching";
import { componentsWithService } from "../../common/config/components_with_service";
import { domainIcon } from "../../common/entity/domain_icon";
import { computeDomain } from "../../common/entity/compute_domain";
import { domainToName } from "../../data/integration";
import { QuickBarParams } from "./show-dialog-quick-bar";
import { HassEntity } from "home-assistant-js-websocket";
import { compare } from "../../common/string/compare";
import memoizeOne from "memoize-one";
interface CommandItem extends ServiceCallRequest {
text: string;
}
@customElement("ha-quick-bar")
export class QuickBar extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _commandItems: CommandItem[] = [];
@internalProperty() private _itemFilter = "";
@internalProperty() private _opened = false;
@internalProperty() private _commandMode = false;
public async showDialog(params: QuickBarParams) {
this._commandMode = params.commandMode || false;
this._opened = true;
this._commandItems = this._generateCommandItems();
}
public closeDialog() {
this._opened = false;
this._itemFilter = "";
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._opened) {
return html``;
}
return html`
<ha-dialog .heading=${true} open @closed=${this.closeDialog} hideActions>
<paper-input
dialogInitialFocus
no-label-float
slot="heading"
class="heading"
@value-changed=${this._entityFilterChanged}
.label=${this.hass.localize(
"ui.dialogs.quick-bar.filter_placeholder"
)}
type="search"
value=${this._commandMode ? `>${this._itemFilter}` : this._itemFilter}
></paper-input>
<mwc-list>
${this._commandMode
? this.renderCommandsList(this._itemFilter)
: this.renderEntityList(this._itemFilter)}
</mwc-list>
</ha-dialog>
`;
}
protected renderCommandsList = memoizeOne((filter) => {
const items = this._filterCommandItems(this._commandItems, filter);
return html`
${items.map(
({ text, domain, service, serviceData }) => html`
<mwc-list-item
@click=${this._executeCommand}
.domain=${domain}
.service=${service}
.serviceData=${serviceData}
graphic="icon"
>
<ha-icon .icon=${domainIcon(domain)} slot="graphic"></ha-icon>
${text}
</mwc-list-item>
`
)}
`;
});
protected renderEntityList = memoizeOne((filter) => {
const entities = this._filterEntityItems(
Object.keys(this.hass.states),
filter
);
return html`
${entities.map((entity_id) => {
const domain = computeDomain(entity_id);
return html`
<mwc-list-item @click=${this._entityMoreInfo} graphic="icon">
<ha-icon .icon=${domainIcon(domain)} slot="graphic"></ha-icon>
${entity_id}
</mwc-list-item>
`;
})}
`;
});
private _entityFilterChanged(ev: PolymerChangedEvent<string>) {
const newFilter = ev.detail.value;
if (newFilter.startsWith(">")) {
this._commandMode = true;
this._itemFilter = newFilter.substring(1);
} else {
this._commandMode = false;
this._itemFilter = newFilter;
}
}
private _generateCommandItems(): CommandItem[] {
const reloadableDomains = componentsWithService(this.hass, "reload").sort();
return reloadableDomains.map((domain) => ({
text:
this.hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
this.hass.localize(
"ui.dialogs.quick-bar.commands.reload.reload",
"domain",
domainToName(this.hass.localize, domain)
),
domain,
service: "reload",
}));
}
private _filterCommandItems(
items: CommandItem[],
filter: string
): CommandItem[] {
return items
.filter(({ text }) =>
fuzzySequentialMatch(filter.toLowerCase(), text.toLowerCase())
)
.sort((itemA, itemB) => compare(itemA.text, itemB.text));
}
private _filterEntityItems(
entity_ids: HassEntity["entity_id"][],
filter: string
): HassEntity["entity_id"][] {
return entity_ids
.filter((entity_id) =>
fuzzySequentialMatch(filter.toLowerCase(), entity_id)
)
.sort();
}
private async _executeCommand(ev: Event) {
const target = ev.currentTarget as any;
await this.hass.callService(
target.domain,
target.service,
target.serviceData
);
this.closeDialog();
}
private _entityMoreInfo(ev: Event) {
ev.preventDefault();
fireEvent(this, "hass-more-info", {
entityId: (ev.target as any).text,
});
this.closeDialog();
}
static get styles() {
return [
haStyleDialog,
css`
.heading {
padding: 20px 20px 0px;
}
ha-dialog {
--dialog-z-index: 8;
--dialog-content-padding: 0px 24px 20px;
}
@media (min-width: 800px) {
ha-dialog {
--mdc-dialog-max-width: 800px;
--mdc-dialog-min-width: 500px;
--dialog-surface-position: fixed;
--dialog-surface-top: 40px;
--mdc-dialog-max-height: calc(100% - 72px);
}
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-quick-bar": QuickBar;
}
}

View File

@@ -0,0 +1,20 @@
import { fireEvent } from "../../common/dom/fire_event";
export interface QuickBarParams {
entityFilter?: string;
commandMode?: boolean;
}
export const loadQuickBar = () =>
import(/* webpackChunkName: "quick-bar-dialog" */ "./ha-quick-bar");
export const showQuickBar = (
element: HTMLElement,
dialogParams: QuickBarParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "ha-quick-bar",
dialogImport: loadQuickBar,
dialogParams,
});
};

View File

@@ -57,7 +57,7 @@ export class HaVoiceCommandDialog extends LitElement {
@internalProperty() private _agentInfo?: AgentInfo;
@query("#messages") private messages!: PaperDialogScrollableElement;
@query("#messages", true) private messages!: PaperDialogScrollableElement;
private recognition!: SpeechRecognition;

View File

@@ -35,7 +35,10 @@ function setProperties(properties) {
setCustomPanelProperties(panelEl, properties);
}
function initialize(panel: CustomPanelInfo, properties: {}) {
function initialize(
panel: CustomPanelInfo,
properties: Record<string, unknown>
) {
const style = document.createElement("style");
style.innerHTML = "body{margin:0}";
document.head.appendChild(style);

View File

@@ -64,7 +64,13 @@ export class ExternalAuth extends Auth {
});
}
private _tokenCallbackPromise?: Promise<RefreshTokenResponse>;
public async refreshAccessToken(force?: boolean) {
if (this._tokenCallbackPromise && !force) {
await this._tokenCallbackPromise;
return;
}
const payload: GetExternalAuthPayload = {
callback: CALLBACK_SET_TOKEN,
};
@@ -72,14 +78,15 @@ export class ExternalAuth extends Auth {
payload.force = true;
}
const callbackPromise = new Promise<RefreshTokenResponse>(
this._tokenCallbackPromise = new Promise<RefreshTokenResponse>(
(resolve, reject) => {
window[CALLBACK_SET_TOKEN] = (success, data) =>
success ? resolve(data) : reject(data);
}
);
await 0;
// we sleep 1 microtask to get the promise to actually set it on the window object.
await Promise.resolve();
if (window.externalApp) {
window.externalApp.getExternalAuth(JSON.stringify(payload));
@@ -87,10 +94,11 @@ export class ExternalAuth extends Auth {
window.webkit!.messageHandlers.getExternalAuth.postMessage(payload);
}
const tokens = await callbackPromise;
const tokens = await this._tokenCallbackPromise;
this.data.access_token = tokens.access_token;
this.data.expires = tokens.expires_in * 1000 + Date.now();
this._tokenCallbackPromise = undefined;
}
public async revoke() {
@@ -101,7 +109,8 @@ export class ExternalAuth extends Auth {
success ? resolve(data) : reject(data);
});
await 0;
// we sleep 1 microtask to get the promise to actually set it on the window object.
await Promise.resolve();
if (window.externalApp) {
window.externalApp.revokeExternalAuth(JSON.stringify(payload));

View File

@@ -1,4 +1,4 @@
<meta name='viewport' content='width=device-width, user-scalable=no, viewport-fit=cover'>
<meta name='viewport' content='width=device-width, viewport-fit=cover'>
<style>
body {
font-family: Roboto, sans-serif;

View File

@@ -44,17 +44,17 @@
<%= renderTemplate('_js_base') %>
<%= renderTemplate('_preload_roboto') %>
<script type="module" crossorigin="use-credentials">
import "<%= latestPageJS %>";
<script crossorigin="use-credentials">
import("<%= latestPageJS %>");
window.latestJS = true;
window.providersPromise = fetch("/auth/providers", {
credentials: "same-origin",
});
</script>
<script nomodule>
<script>
(function() {
// Safari 10.1 supports type=module but ignores nomodule, so we add this check.
if (!isS101) {
if (!window.latestJS) {
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5PageJS %>");

View File

@@ -52,17 +52,17 @@
<%= renderTemplate('_js_base') %>
<%= renderTemplate('_preload_roboto') %>
<script type="module" crossorigin="use-credentials">
import "<%= latestPageJS %>";
<script crossorigin="use-credentials">
import("<%= latestPageJS %>");
window.latestJS = true;
window.stepsPromise = fetch("/api/onboarding", {
credentials: "same-origin",
});
</script>
<script nomodule>
<script>
(function() {
// Safari 10.1 supports type=module but ignores nomodule, so we add this check.
if (!isS101) {
if (!window.latestJS) {
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5PageJS %>");

View File

@@ -99,7 +99,7 @@ export class HaTabsSubpageDataTable extends LitElement {
*/
@property() public tabs!: PageNavigation[];
@query("ha-data-table") private _dataTable!: HaDataTable;
@query("ha-data-table", true) private _dataTable!: HaDataTable;
public clearSelection() {
this._dataTable.clearSelection();

View File

@@ -17,9 +17,10 @@ import {
import "./ha-init-page";
import "./home-assistant-main";
import { storeState } from "../util/ha-pref-storage";
import QuickBarMixin from "../state/quick-bar-mixin";
@customElement("home-assistant")
export class HomeAssistantAppEl extends HassElement {
export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
@internalProperty() private _route?: Route;
@internalProperty() private _error = false;

View File

@@ -33,7 +33,7 @@ class NotificationManager extends LitElement {
@internalProperty() private _noCancelOnOutsideClick = false;
@query("ha-toast") private _toast!: HaToast;
@query("ha-toast", true) private _toast!: HaToast;
public async showDialog({
message,

View File

@@ -0,0 +1,26 @@
import { LitElement } from "lit-element";
import { Constructor } from "../types";
export const KeyboardShortcutMixin = <T extends Constructor<LitElement>>(
superClass: T
) =>
class extends superClass {
private _keydownEvent = (event: KeyboardEvent) => {
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
event.preventDefault();
this.handleKeyboardSave();
}
};
public connectedCallback() {
super.connectedCallback();
this.addEventListener("keydown", this._keydownEvent);
}
public disconnectedCallback() {
this.removeEventListener("keydown", this._keydownEvent);
super.disconnectedCallback();
}
protected handleKeyboardSave() {}
};

View File

@@ -130,7 +130,7 @@ export class HaConfigAreasDashboard extends LitElement {
)}"
@click=${this._createArea}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
</hass-tabs-subpage-data-table>
`;

View File

@@ -133,7 +133,7 @@ export default class HaAutomationActionRow extends LitElement {
)}
@click=${this._moveUp}
>
<ha-svg-icon path=${mdiArrowUp}></ha-svg-icon>
<ha-svg-icon .path=${mdiArrowUp}></ha-svg-icon>
</mwc-icon-button>
`
: ""}
@@ -148,7 +148,7 @@ export default class HaAutomationActionRow extends LitElement {
)}
@click=${this._moveDown}
>
<ha-svg-icon path=${mdiArrowDown}></ha-svg-icon>
<ha-svg-icon .path=${mdiArrowDown}></ha-svg-icon>
</mwc-icon-button>
`
: ""}
@@ -157,7 +157,7 @@ export default class HaAutomationActionRow extends LitElement {
slot="trigger"
.title=${this.hass.localize("ui.common.menu")}
.label=${this.hass.localize("ui.common.overflow_menu")}
><ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon>
><ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item .disabled=${!this._uiModeAvailable}>
${yamlMode

View File

@@ -40,7 +40,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
)}
>
<ha-svg-icon path=${mdiDelete}></ha-svg-icon>
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
</mwc-icon-button>
<div class="card-content">
<h2>

View File

@@ -21,7 +21,7 @@ export class HaEventAction extends LitElement implements ActionElement {
@property() public action!: EventAction;
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
@query("ha-yaml-editor", true) private _yamlEditor?: HaYamlEditor;
private _actionData?: EventAction["event_data"];

View File

@@ -34,7 +34,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
@property({ attribute: false }) public action!: ServiceAction;
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
@query("ha-yaml-editor", true) private _yamlEditor?: HaYamlEditor;
private _actionData?: ServiceAction["data"];

View File

@@ -40,7 +40,9 @@ export class HaWaitAction extends LitElement implements ActionElement {
></paper-input>
<br />
<ha-formfield
.label=${this.hass.localize("ui.panel.config.automation.editor.actions.type.wait_template.continue_timeout")}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.wait_template.continue_timeout"
)}
>
<ha-switch
.checked=${continue_on_timeout}

View File

@@ -71,7 +71,7 @@ export default class HaAutomationConditionRow extends LitElement {
.title=${this.hass.localize("ui.common.menu")}
.label=${this.hass.localize("ui.common.overflow_menu")}
slot="trigger"
><ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon
><ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon
></mwc-icon-button>
<mwc-list-item>
${this._yamlMode

View File

@@ -1,9 +1,17 @@
import "@material/mwc-fab";
import { mdiContentDuplicate, mdiContentSave, mdiDelete } from "@mdi/js";
import {
mdiCheck,
mdiContentDuplicate,
mdiContentSave,
mdiDelete,
mdiDotsVertical,
} from "@mdi/js";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@polymer/paper-input/paper-textarea";
import "@material/mwc-list/mwc-list-item";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import { PaperListboxElement } from "@polymer/paper-listbox";
import {
css,
@@ -14,12 +22,16 @@ import {
property,
PropertyValues,
TemplateResult,
query,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { navigate } from "../../../common/navigate";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import "../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../components/ha-yaml-editor";
import {
AutomationConfig,
AutomationEntity,
@@ -37,6 +49,7 @@ import {
} from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/ha-app-layout";
import "../../../layouts/hass-tabs-subpage";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
@@ -59,7 +72,7 @@ declare global {
}
}
export class HaAutomationEditor extends LitElement {
export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public automationId!: string;
@@ -80,6 +93,10 @@ export class HaAutomationEditor extends LitElement {
@internalProperty() private _entityId?: string;
@internalProperty() private _mode: "gui" | "yaml" = "gui";
@query("ha-yaml-editor", true) private _editor?: HaYamlEditor;
protected render(): TemplateResult {
const stateObj = this._entityId
? this.hass.states[this._entityId]
@@ -92,29 +109,82 @@ export class HaAutomationEditor extends LitElement {
.backCallback=${() => this._backTapped()}
.tabs=${configSections.automation}
>
${!this.automationId
? ""
: html`
<mwc-icon-button
slot="toolbar-icon"
title="${this.hass.localize(
"ui.panel.config.automation.picker.duplicate_automation"
)}"
@click=${this._duplicate}
>
<ha-svg-icon .path=${mdiContentDuplicate}></ha-svg-icon>
</mwc-icon-button>
<mwc-icon-button
class="warning"
slot="toolbar-icon"
title="${this.hass.localize(
"ui.panel.config.automation.picker.delete_automation"
)}"
@click=${this._deleteConfirm}
>
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
</mwc-icon-button>
`}
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@action=${this._handleMenuAction}
activatable
>
<mwc-icon-button
slot="trigger"
.title=${this.hass.localize("ui.common.menu")}
.label=${this.hass.localize("ui.common.overflow_menu")}
><ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item
aria-label=${this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
)}
graphic="icon"
?activated=${this._mode === "gui"}
>
${this.hass.localize("ui.panel.config.automation.editor.edit_ui")}
${this._mode === "gui"
? html`<ha-svg-icon
slot="graphic"
.path=${mdiCheck}
></ha-svg-icon>`
: ``}
</mwc-list-item>
<mwc-list-item
aria-label=${this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
graphic="icon"
?activated=${this._mode === "yaml"}
>
${this.hass.localize("ui.panel.config.automation.editor.edit_yaml")}
${this._mode === "yaml"
? html`<ha-svg-icon
slot="graphic"
.path=${mdiCheck}
></ha-svg-icon>`
: ``}
</mwc-list-item>
<li divider role="separator"></li>
<mwc-list-item
.disabled=${!this.automationId}
aria-label=${this.hass.localize(
"ui.panel.config.automation.picker.duplicate_automation"
)}
graphic="icon"
>
${this.hass.localize(
"ui.panel.config.automation.picker.duplicate_automation"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</mwc-list-item>
<mwc-list-item
.disabled=${!this.automationId}
aria-label=${this.hass.localize(
"ui.panel.config.automation.picker.delete_automation"
)}
class=${classMap({ warning: this.automationId })}
graphic="icon"
>
${this.hass.localize(
"ui.panel.config.automation.picker.delete_automation"
)}
<ha-svg-icon slot="graphic" .path=${mdiDelete}></ha-svg-icon>
</mwc-list-item>
</ha-button-menu>
${this._config
? html`
${this.narrow
@@ -124,215 +194,270 @@ export class HaAutomationEditor extends LitElement {
${this._errors
? html` <div class="errors">${this._errors}</div> `
: ""}
<ha-config-section .isWide=${this.isWide}>
${!this.narrow
? html` <span slot="header">${this._config.alias}</span> `
: ""}
<span slot="introduction">
${this.hass.localize(
"ui.panel.config.automation.editor.introduction"
)}
</span>
<ha-card>
<div class="card-content">
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.alias"
)}
name="alias"
.value=${this._config.alias}
@value-changed=${this._valueChanged}
>
</paper-input>
<paper-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.description.label"
)}
.placeholder=${this.hass.localize(
"ui.panel.config.automation.editor.description.placeholder"
)}
name="description"
.value=${this._config.description}
@value-changed=${this._valueChanged}
></paper-textarea>
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.modes.description",
"documentation_link",
html`<a
${this._mode === "gui"
? html`
<ha-config-section .isWide=${this.isWide}>
${!this.narrow
? html`
<span slot="header">${this._config.alias}</span>
`
: ""}
<span slot="introduction">
${this.hass.localize(
"ui.panel.config.automation.editor.introduction"
)}
</span>
<ha-card>
<div class="card-content">
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.alias"
)}
name="alias"
.value=${this._config.alias}
@value-changed=${this._valueChanged}
>
</paper-input>
<paper-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.description.label"
)}
.placeholder=${this.hass.localize(
"ui.panel.config.automation.editor.description.placeholder"
)}
name="description"
.value=${this._config.description}
@value-changed=${this._valueChanged}
></paper-textarea>
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.modes.description",
"documentation_link",
html`<a
href="${documentationUrl(
this.hass,
"/integrations/automation/#automation-modes"
)}"
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.automation.editor.modes.documentation"
)}</a
>`
)}
</p>
<paper-dropdown-menu-light
.label=${this.hass.localize(
"ui.panel.config.automation.editor.modes.label"
)}
no-animations
>
<paper-listbox
slot="dropdown-content"
.selected=${this._config.mode
? MODES.indexOf(this._config.mode)
: 0}
@iron-select=${this._modeChanged}
>
${MODES.map(
(mode) => html`
<paper-item .mode=${mode}>
${this.hass.localize(
`ui.panel.config.automation.editor.modes.${mode}`
) || mode}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
${this._config.mode &&
MODES_MAX.includes(this._config.mode)
? html`<paper-input
.label=${this.hass.localize(
`ui.panel.config.automation.editor.max.${this._config.mode}`
)}
type="number"
name="max"
.value=${this._config.max || "10"}
@value-changed=${this._valueChanged}
>
</paper-input>`
: html``}
</div>
${stateObj
? html`
<div
class="card-actions layout horizontal justified center"
>
<div class="layout horizontal center">
<ha-entity-toggle
.hass=${this.hass}
.stateObj=${stateObj}
></ha-entity-toggle>
${this.hass.localize(
"ui.panel.config.automation.editor.enable_disable"
)}
</div>
<mwc-button
@click=${this._excuteAutomation}
.stateObj=${stateObj}
>
${this.hass.localize(
"ui.card.automation.trigger"
)}
</mwc-button>
</div>
`
: ""}
</ha-card>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.introduction"
)}
</p>
<a
href="${documentationUrl(
this.hass,
"/integrations/automation/#automation-modes"
"/docs/automation/trigger/"
)}"
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.automation.editor.modes.documentation"
)}</a
>`
)}
</p>
<paper-dropdown-menu-light
.label=${this.hass.localize(
"ui.panel.config.automation.editor.modes.label"
)}
no-animations
>
<paper-listbox
slot="dropdown-content"
.selected=${this._config.mode
? MODES.indexOf(this._config.mode)
: 0}
@iron-select=${this._modeChanged}
>
${MODES.map(
(mode) => html`
<paper-item .mode=${mode}>
${this.hass.localize(
`ui.panel.config.automation.editor.modes.${mode}`
) || mode}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
${this._config.mode &&
MODES_MAX.includes(this._config.mode)
? html` <paper-input
.label=${this.hass.localize(
`ui.panel.config.automation.editor.max.${this._config.mode}`
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.learn_more"
)}
type="number"
name="max"
.value=${this._config.max || "10"}
@value-changed=${this._valueChanged}
</a>
</span>
<ha-automation-trigger
.triggers=${this._config.trigger}
@value-changed=${this._triggerChanged}
.hass=${this.hass}
></ha-automation-trigger>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.introduction"
)}
</p>
<a
href="${documentationUrl(
this.hass,
"/docs/scripts/conditions/"
)}"
target="_blank"
rel="noreferrer"
>
</paper-input>`
: html``}
</div>
${stateObj
? html`
<div
class="card-actions layout horizontal justified center"
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.learn_more"
)}
</a>
</span>
<ha-automation-condition
.conditions=${this._config.condition || []}
@value-changed=${this._conditionChanged}
.hass=${this.hass}
></ha-automation-condition>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.introduction"
)}
</p>
<a
href="${documentationUrl(
this.hass,
"/docs/automation/action/"
)}"
target="_blank"
rel="noreferrer"
>
<div class="layout horizontal center">
<ha-entity-toggle
.hass=${this.hass}
.stateObj=${stateObj}
></ha-entity-toggle>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.learn_more"
)}
</a>
</span>
<ha-automation-action
.actions=${this._config.action}
@value-changed=${this._actionChanged}
.hass=${this.hass}
></ha-automation-action>
</ha-config-section>
`
: this._mode === "yaml"
? html`
<ha-config-section .isWide=${false}>
${!this.narrow
? html`
<span slot="header">${this._config.alias}</span>
`
: ``}
<ha-card>
<div class="card-content">
<ha-yaml-editor
.defaultValue=${this._preprocessYaml()}
@value-changed=${this._yamlChanged}
></ha-yaml-editor>
<mwc-button @click=${this._copyYaml}>
${this.hass.localize(
"ui.panel.config.automation.editor.enable_disable"
)}
</div>
<mwc-button
@click=${this._excuteAutomation}
.stateObj=${stateObj}
>
${this.hass.localize(
"ui.card.automation.trigger"
"ui.panel.config.automation.editor.copy_to_clipboard"
)}
</mwc-button>
</div>
`
: ""}
</ha-card>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.introduction"
)}
</p>
<a
href="${documentationUrl(
this.hass,
"/docs/automation/trigger/"
)}"
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.learn_more"
)}
</a>
</span>
<ha-automation-trigger
.triggers=${this._config.trigger}
@value-changed=${this._triggerChanged}
.hass=${this.hass}
></ha-automation-trigger>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.introduction"
)}
</p>
<a
href="${documentationUrl(
this.hass,
"/docs/scripts/conditions/"
)}"
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.learn_more"
)}
</a>
</span>
<ha-automation-condition
.conditions=${this._config.condition || []}
@value-changed=${this._conditionChanged}
.hass=${this.hass}
></ha-automation-condition>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.introduction"
)}
</p>
<a
href="${documentationUrl(
this.hass,
"/docs/automation/action/"
)}"
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.learn_more"
)}
</a>
</span>
<ha-automation-action
.actions=${this._config.action}
@value-changed=${this._actionChanged}
.hass=${this.hass}
></ha-automation-action>
</ha-config-section>
${stateObj
? html`
<div
class="card-actions layout horizontal justified center"
>
<div class="layout horizontal center">
<ha-entity-toggle
.hass=${this.hass}
.stateObj=${stateObj}
></ha-entity-toggle>
${this.hass.localize(
"ui.panel.config.automation.editor.enable_disable"
)}
</div>
<mwc-button
@click=${this._excuteAutomation}
.stateObj=${stateObj}
>
${this.hass.localize(
"ui.card.automation.trigger"
)}
</mwc-button>
</div>
`
: ""}
</ha-card>
<ha-config-section> </ha-config-section
></ha-config-section>
`
: ``}
</div>
`
: ""}
@@ -342,7 +467,7 @@ export class HaAutomationEditor extends LitElement {
.title=${this.hass.localize("ui.panel.config.automation.editor.save")}
@click=${this._saveAutomation}
>
<ha-svg-icon slot="icon" path=${mdiContentSave}></ha-svg-icon>
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
</mwc-fab>
</hass-tabs-subpage>
`;
@@ -484,6 +609,33 @@ export class HaAutomationEditor extends LitElement {
triggerAutomation(this.hass, (ev.target as any).stateObj.entity_id);
}
private _preprocessYaml() {
const cleanConfig = this._config;
if (!cleanConfig) {
return {};
}
delete cleanConfig.id;
return cleanConfig;
}
private async _copyYaml() {
if (this._editor?.yaml) {
navigator.clipboard.writeText(this._editor.yaml);
}
}
private _yamlChanged(ev: CustomEvent) {
ev.stopPropagation();
if (!ev.detail.isValid) {
return;
}
this._config = ev.detail.value;
this._errors = undefined;
this._dirty = true;
}
private _backTapped(): void {
if (this._dirty) {
showConfirmationDialog(this, {
@@ -540,6 +692,23 @@ export class HaAutomationEditor extends LitElement {
history.back();
}
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._mode = "gui";
break;
case 1:
this._mode = "yaml";
break;
case 2:
this._duplicate();
break;
case 3:
this._deleteConfirm();
break;
}
}
private _saveAutomation(): void {
const id = this.automationId || String(Date.now());
this.hass!.callApi(
@@ -561,6 +730,10 @@ export class HaAutomationEditor extends LitElement {
);
}
protected handleKeyboardSave() {
this._saveAutomation();
}
static get styles(): CSSResult[] {
return [
haStyle,

View File

@@ -1,5 +1,6 @@
import "@material/mwc-fab";
import { mdiPlus } from "@mdi/js";
import "@material/mwc-icon-button";
import { mdiPlus, mdiHelpCircle } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import {
CSSResult,
@@ -16,8 +17,8 @@ import { formatDateTime } from "../../../common/datetime/format_date_time";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../components/entity/ha-entity-toggle";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import {
AutomationConfig,
@@ -31,6 +32,7 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import { showThingtalkDialog } from "./show-dialog-thingtalk";
import { documentationUrl } from "../../../util/documentation-url";
@customElement("ha-automation-picker")
class HaAutomationPicker extends LitElement {
@@ -169,6 +171,9 @@ class HaAutomationPicker extends LitElement {
)}
hasFab
>
<mwc-icon-button slot="toolbar-icon" @click=${this._showHelp}>
<ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon>
</mwc-icon-button>
<mwc-fab
slot="fab"
title=${this.hass.localize(
@@ -176,7 +181,7 @@ class HaAutomationPicker extends LitElement {
)}
@click=${this._createNew}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
</hass-tabs-subpage-data-table>
`;
@@ -188,6 +193,26 @@ class HaAutomationPicker extends LitElement {
fireEvent(this, "hass-more-info", { entityId });
}
private _showHelp() {
showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.automation.caption"),
text: html`
${this.hass.localize("ui.panel.config.automation.picker.introduction")}
<p>
<a
href="${documentationUrl(this.hass, "/docs/automation/editor/")}"
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.automation.picker.learn_more"
)}
</a>
</p>
`,
});
}
private _execute(ev) {
const entityId = ev.currentTarget.automation.entity_id;
triggerAutomation(this.hass, entityId);

View File

@@ -50,7 +50,7 @@ class DialogThingtalk extends LitElement {
@internalProperty() private _placeholders?: PlaceholderContainer;
@query("#input") private _input?: PaperInputElement;
@query("#input", true) private _input?: PaperInputElement;
private _value!: string;
@@ -133,10 +133,13 @@ class DialogThingtalk extends LitElement {
Skip
</mwc-button>
<mwc-button @click="${this._generate}" .disabled=${this._submitting}>
<ha-circular-progress
?active="${this._submitting}"
alt="Creating your automation..."
></ha-circular-progress>
${this._submitting
? html`<ha-circular-progress
active
size="small"
title="Creating your automation..."
></ha-circular-progress>`
: ""}
Create automation
</mwc-button>
</div>
@@ -246,17 +249,6 @@ class DialogThingtalk extends LitElement {
mwc-button.left {
margin-right: auto;
}
mwc-button ha-circular-progress {
width: 14px;
height: 14px;
margin-right: 20px;
}
ha-circular-progress {
display: none;
}
ha-circular-progress[active] {
display: block;
}
.error {
color: var(--error-color);
}

View File

@@ -102,7 +102,7 @@ export default class HaAutomationTriggerRow extends LitElement {
slot="trigger"
.title=${this.hass.localize("ui.common.menu")}
.label=${this.hass.localize("ui.common.overflow_menu")}
><ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon
><ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon
></mwc-icon-button>
<mwc-list-item .disabled=${selected === -1}>
${yamlMode

View File

@@ -89,7 +89,7 @@ export class CloudWebhooks extends LitElement {
`
: this._localHooks.map(
(entry) => html`
<ha-settings-row .narrow=${this.narrow}>
<ha-settings-row .narrow=${this.narrow} .entry=${entry}>
<span slot="heading">
${entry.name}
${entry.domain !== entry.name.toLowerCase()
@@ -157,7 +157,7 @@ export class CloudWebhooks extends LitElement {
}
private async _enableWebhook(ev: MouseEvent) {
const entry = (ev.currentTarget as any).parentElement.entry;
const entry = (ev.currentTarget as any).parentElement!.entry as Webhook;
this._progress = [...this._progress, entry.webhook_id];
let updatedWebhook;

View File

@@ -131,7 +131,7 @@ class CloudAlexa extends LitElement {
"not-exposed": !isExposed,
})}
.disabled=${!emptyFilter}
.title=${this.hass!.localize("ui.panel.config.cloud.google.expose")}
.title=${this.hass!.localize("ui.panel.config.cloud.alexa.expose")}
>
<ha-svg-icon
.path=${config.should_expose !== null
@@ -169,7 +169,7 @@ class CloudAlexa extends LitElement {
${iconButton}
<mwc-list-item hasMeta>
${this.hass!.localize(
"ui.panel.config.cloud.google.expose_entity"
"ui.panel.config.cloud.alexa.expose_entity"
)}
<ha-svg-icon
class="exposed"
@@ -179,7 +179,7 @@ class CloudAlexa extends LitElement {
</mwc-list-item>
<mwc-list-item hasMeta>
${this.hass!.localize(
"ui.panel.config.cloud.google.dont_expose_entity"
"ui.panel.config.cloud.alexa.dont_expose_entity"
)}
<ha-svg-icon
class="not-exposed"
@@ -189,7 +189,7 @@ class CloudAlexa extends LitElement {
</mwc-list-item>
<mwc-list-item hasMeta>
${this.hass!.localize(
"ui.panel.config.cloud.google.follow_domain"
"ui.panel.config.cloud.alexa.follow_domain"
)}
<ha-svg-icon
class=${classMap({

View File

@@ -13,7 +13,7 @@ import { classMap } from "lit-html/directives/class-map";
@customElement("mqtt-discovery-payload")
class MQTTDiscoveryPayload extends LitElement {
@property() public payload!: object;
@property() public payload!: Record<string, unknown>;
@property() public showAsYaml = false;

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