diff --git a/hassio/src/addon-store/hassio-addon-repository.ts b/hassio/src/addon-store/hassio-addon-repository.ts index 722697ecd2..accfe7d8ed 100644 --- a/hassio/src/addon-store/hassio-addon-repository.ts +++ b/hassio/src/addon-store/hassio-addon-repository.ts @@ -39,81 +39,87 @@ class HassioAddonRepositoryEl extends LitElement { protected render(): TemplateResult { const repo = this.repo; const addons = this._getAddons(this.addons, this.filter); + const ha105pluss = this._computeHA105plus; if (this.filter && addons.length < 1) { return html` -
-
-
- No results found in "${repo.name}" -
-
+
+

+ No results found in "${repo.name}" +

`; } return html` -
-
+
+

${repo.name} -
- Maintained by ${repo.maintainer}
- ${repo.url} -
+

+

+ Maintained by ${repo.maintainer}
+ ${repo.url} +

+
+ ${addons.map( + (addon) => html` + +
+ +
+
+ ` + )}
- - ${addons.map( - (addon) => html` - -
- -
-
- ` - )}
`; } - private computeIcon(addon) { - return addon.installed && addon.installed !== addon.version - ? "hassio:arrow-up-bold-circle" - : "hassio:puzzle"; - } - - private computeIconTitle(addon) { - if (addon.installed) { - return addon.installed !== addon.version - ? "New version available" - : "Add-on is installed"; - } - return addon.available - ? "Add-on is not installed" - : "Add-on is not available on your system"; - } - - private computeIconClass(addon) { - if (addon.installed) { - return addon.installed !== addon.version ? "update" : "installed"; - } - return !addon.available ? "not_available" : ""; - } - - private addonTapped(ev) { + private _addonTapped(ev) { navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`); } + private get _computeHA105plus(): boolean { + const [major, minor] = this.hass.config.version.split(".", 2); + return Number(major) > 0 || (major === "0" && Number(minor) >= 105); + } + static get styles(): CSSResultArray { return [ hassioStyle, diff --git a/hassio/src/addon-store/hassio-repositories-editor.ts b/hassio/src/addon-store/hassio-repositories-editor.ts index 5110a77c45..27241482b4 100644 --- a/hassio/src/addon-store/hassio-repositories-editor.ts +++ b/hassio/src/addon-store/hassio-repositories-editor.ts @@ -36,61 +36,63 @@ class HassioRepositoriesEditor extends LitElement { protected render(): TemplateResult { const repos = this._sortedRepos(this.repos); return html` -
-
+
+

Repositories -
- Configure which add-on repositories to fetch data from: -
-

- ${// Use repeat so that the fade-out from call-service-api-button - // stays with the correct repo after we add/delete one. - repeat( - repos, - (repo) => repo.slug, - (repo) => html` - -
- -
-
- - Remove - -
-
- ` - )} + +

+ Configure which add-on repositories to fetch data from: +

+
+ ${// Use repeat so that the fade-out from call-service-api-button + // stays with the correct repo after we add/delete one. + repeat( + repos, + (repo) => repo.slug, + (repo) => html` + +
+ +
+
+ + Remove + +
+
+ ` + )} - -
- - -
-
- - Add - -
-
+ +
+ + +
+
+ + Add + +
+
+
`; } diff --git a/hassio/src/addon-view/hassio-addon-audio.ts b/hassio/src/addon-view/hassio-addon-audio.ts index c0718a4c9a..84357963b4 100644 --- a/hassio/src/addon-view/hassio-addon-audio.ts +++ b/hassio/src/addon-view/hassio-addon-audio.ts @@ -51,7 +51,7 @@ class HassioAddonAudio extends LitElement { { return html` - ${item.name} + ${item.name} `; })} { return html` - ${item.name} + ${item.name} `; })} @@ -123,17 +127,13 @@ class HassioAddonAudio extends LitElement { } private _setInputDevice(ev): void { - const device = ev.detail.device; - if (device) { - this._selectedInput = device; - } + const device = ev.detail.item.getAttribute("device"); + this._selectedInput = device || null; } private _setOutputDevice(ev): void { - const device = ev.detail.device; - if (device) { - this._selectedOutput = device; - } + const device = ev.detail.item.getAttribute("device"); + this._selectedOutput = device || null; } private async _addonChanged(): Promise { @@ -143,13 +143,11 @@ class HassioAddonAudio extends LitElement { return; } - const noDevice: HassioHardwareAudioDevice[] = [ - { device: undefined, name: "-" }, - ]; + const noDevice: HassioHardwareAudioDevice = { device: null, name: "-" }; try { const { audio } = await fetchHassioHardwareAudio(this.hass); - const inupt = Object.keys(audio.input).map((key) => ({ + const input = Object.keys(audio.input).map((key) => ({ device: key, name: audio.input[key], })); @@ -158,12 +156,12 @@ class HassioAddonAudio extends LitElement { name: audio.output[key], })); - this._inputDevices = noDevice.concat(inupt); - this._outputDevices = noDevice.concat(output); + this._inputDevices = [noDevice, ...input]; + this._outputDevices = [noDevice, ...output]; } catch { this._error = "Failed to fetch audio hardware"; - this._inputDevices = noDevice; - this._outputDevices = noDevice; + this._inputDevices = [noDevice]; + this._outputDevices = [noDevice]; } } diff --git a/hassio/src/addon-view/hassio-addon-config.ts b/hassio/src/addon-view/hassio-addon-config.ts index 5d36267a81..1a91052163 100644 --- a/hassio/src/addon-view/hassio-addon-config.ts +++ b/hassio/src/addon-view/hassio-addon-config.ts @@ -10,6 +10,7 @@ import { property, PropertyValues, TemplateResult, + query, } from "lit-element"; import { HomeAssistant } from "../../../src/types"; @@ -20,30 +21,42 @@ import { } from "../../../src/data/hassio/addon"; import { hassioStyle } from "../resources/hassio-style"; import { haStyle } from "../../../src/resources/styles"; -import { PolymerChangedEvent } from "../../../src/polymer-types"; import { fireEvent } from "../../../src/common/dom/fire_event"; +import "../../../src/components/ha-yaml-editor"; +// tslint:disable-next-line: no-duplicate-imports +import { HaYamlEditor } from "../../../src/components/ha-yaml-editor"; +import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box"; @customElement("hassio-addon-config") class HassioAddonConfig extends LitElement { @property() public hass!: HomeAssistant; @property() public addon!: HassioAddonDetails; @property() private _error?: string; - @property() private _config!: string; @property({ type: Boolean }) private _configHasChanged = false; + @query("ha-yaml-editor") private _editor!: HaYamlEditor; + protected render(): TemplateResult { + const editor = this._editor; + // If editor not rendered, don't show the error. + const valid = editor ? editor.isValid : true; + return html`
+ ${this._error ? html`
${this._error}
` : ""} - + ${valid + ? "" + : html` +
Invalid YAML
+ `}
@@ -51,7 +64,7 @@ class HassioAddonConfig extends LitElement { Save @@ -77,7 +90,7 @@ class HassioAddonConfig extends LitElement { } .errors { color: var(--google-red-500); - margin-bottom: 16px; + margin-top: 16px; } iron-autogrow-textarea { width: 100%; @@ -93,18 +106,26 @@ class HassioAddonConfig extends LitElement { protected updated(changedProperties: PropertyValues): void { super.updated(changedProperties); if (changedProperties.has("addon")) { - this._config = JSON.stringify(this.addon.options, null, 2); + this._editor.setValue(this.addon.options); } } - private _configChanged(ev: PolymerChangedEvent): void { - this._config = - ev.detail.value || JSON.stringify(this.addon.options, null, 2); - this._configHasChanged = - this._config !== JSON.stringify(this.addon.options, null, 2); + private _configChanged(): void { + this._configHasChanged = true; + this.requestUpdate(); } private async _resetTapped(): Promise { + const confirmed = await showConfirmationDialog(this, { + title: this.addon.name, + text: "Are you sure you want to reset all your options?", + confirmText: "reset options", + }); + + if (!confirmed) { + return; + } + this._error = undefined; const data: HassioAddonSetOptionParams = { options: null, @@ -129,7 +150,7 @@ class HassioAddonConfig extends LitElement { this._error = undefined; try { data = { - options: JSON.parse(this._config), + options: this._editor.value, }; } catch (err) { this._error = err; diff --git a/hassio/src/addon-view/hassio-addon-info.ts b/hassio/src/addon-view/hassio-addon-info.ts index c31de13b80..d8f5ff69db 100644 --- a/hassio/src/addon-view/hassio-addon-info.ts +++ b/hassio/src/addon-view/hassio-addon-info.ts @@ -319,6 +319,7 @@ class HassioAddonInfo extends LitElement {
@@ -326,6 +327,7 @@ class HassioAddonInfo extends LitElement {
${this.addon.ingress @@ -336,6 +338,7 @@ class HassioAddonInfo extends LitElement { @change=${this._panelToggled} .checked=${this.addon.ingress_panel} .disabled=${this._computeCannotIngressSidebar} + haptic > ${this._computeCannotIngressSidebar ? html` @@ -363,6 +366,7 @@ class HassioAddonInfo extends LitElement {
` @@ -450,7 +454,6 @@ class HassioAddonInfo extends LitElement { Install diff --git a/hassio/src/components/hassio-card-content.ts b/hassio/src/components/hassio-card-content.ts index ad13904c20..737c36edc3 100644 --- a/hassio/src/components/hassio-card-content.ts +++ b/hassio/src/components/hassio-card-content.ts @@ -17,21 +17,40 @@ class HassioCardContent extends LitElement { @property() public hass!: HomeAssistant; @property() public title!: string; @property() public description?: string; - @property({ type: Boolean }) public available?: boolean; + @property({ type: Boolean }) public available: boolean = true; + @property({ type: Boolean }) public showTopbar: boolean = false; + @property() public topbarClass?: string; @property() public datetime?: string; @property() public iconTitle?: string; @property() public iconClass?: string; @property() public icon = "hass:help-circle"; + @property() public iconImage?: string; protected render(): TemplateResult { return html` - + ${this.showTopbar + ? html` +
+ ` + : ""} + ${this.iconImage + ? html` +
+ +
+
+ ` + : html` + + `}
-
${this.title}
+
+ ${this.title} +
${this.description} ${/* treat as available when undefined */ @@ -53,8 +72,9 @@ class HassioCardContent extends LitElement { static get styles(): CSSResult { return css` iron-icon { - margin-right: 16px; - margin-top: 16px; + margin-right: 24px; + margin-left: 8px; + margin-top: 12px; float: left; color: var(--secondary-text-color); } @@ -88,6 +108,44 @@ class HassioCardContent extends LitElement { ha-relative-time { display: block; } + .icon_image img { + max-height: 40px; + max-width: 40px; + margin-top: 4px; + margin-right: 16px; + float: left; + } + .icon_image.stopped, + .icon_image.not_available { + filter: grayscale(1); + } + .dot { + position: absolute; + background-color: var(--paper-orange-400); + width: 12px; + height: 12px; + top: 8px; + right: 8px; + border-radius: 50%; + } + .topbar { + position: absolute; + width: 100%; + height: 2px; + top: 0; + left: 0; + border-top-left-radius: 2px; + border-top-right-radius: 2px; + } + .topbar.installed { + background-color: var(--primary-color); + } + .topbar.update { + background-color: var(--accent-color); + } + .topbar.unavailable { + background-color: var(--error-color); + } `; } } diff --git a/hassio/src/dashboard/hassio-addons.ts b/hassio/src/dashboard/hassio-addons.ts index 3d21fc9ea4..801afd1854 100644 --- a/hassio/src/dashboard/hassio-addons.ts +++ b/hassio/src/dashboard/hassio-addons.ts @@ -22,38 +22,61 @@ class HassioAddons extends LitElement { @property() public addons?: HassioAddonInfo[]; protected render(): TemplateResult { + const [major, minor] = this.hass.config.version.split(".", 2); + const ha105pluss = + Number(major) > 0 || (major === "0" && Number(minor) >= 105); return html` -
-
Add-ons
- ${!this.addons - ? html` - -
- You don't have any add-ons installed yet. Head over to - the add-on store to - get started! -
-
- ` - : this.addons - .sort((a, b) => (a.name > b.name ? 1 : -1)) - .map( - (addon) => html` - -
- -
-
- ` - )} +
+

Add-ons

+
+ ${!this.addons + ? html` + +
+ You don't have any add-ons installed yet. Head over to + the add-on store + to get started! +
+
+ ` + : this.addons + .sort((a, b) => (a.name > b.name ? 1 : -1)) + .map( + (addon) => html` + +
+ +
+
+ ` + )} +
`; } @@ -70,28 +93,6 @@ class HassioAddons extends LitElement { ]; } - private _computeIcon(addon: HassioAddonInfo): string { - return addon.installed !== addon.version - ? "hassio:arrow-up-bold-circle" - : "hassio:puzzle"; - } - - private _computeIconTitle(addon: HassioAddonInfo): string { - if (addon.installed !== addon.version) { - return "New version available"; - } - return addon.state === "started" - ? "Add-on is running" - : "Add-on is stopped"; - } - - private _computeIconClass(addon: HassioAddonInfo): string { - if (addon.installed !== addon.version) { - return "update"; - } - return addon.state === "started" ? "running" : ""; - } - private _addonTapped(ev: any): void { navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`); } diff --git a/hassio/src/dashboard/hassio-update.ts b/hassio/src/dashboard/hassio-update.ts index e9d699c669..e7e1fe3f96 100644 --- a/hassio/src/dashboard/hassio-update.ts +++ b/hassio/src/dashboard/hassio-update.ts @@ -38,7 +38,12 @@ export class HassioUpdate extends LitElement { this.supervisorInfo, this.hassOsInfo, ].filter((value) => { - return !!value && value.version !== value.last_version; + return ( + !!value && + (value.last_version + ? value.version !== value.last_version + : value.version !== value.version_latest) + ); }).length; if (!updatesAvailable) { @@ -52,14 +57,14 @@ export class HassioUpdate extends LitElement {
Error: ${this._error}
` : ""} +

+ ${updatesAvailable > 1 + ? "Updates Available 🎉" + : "Update Available 🎉"} +

-
- ${updatesAvailable > 1 - ? "Updates Available 🎉" - : "Update Available 🎉"} -
${this._renderUpdateCard( - "Home Assistant", + "Home Assistant Core", this.hassInfo.version, this.hassInfo.last_version, "hassio/homeassistant/update", @@ -69,7 +74,7 @@ export class HassioUpdate extends LitElement { "hassio:home-assistant" )} ${this._renderUpdateCard( - "Hass.io Supervisor", + "Supervisor", this.supervisorInfo.version, this.supervisorInfo.last_version, "hassio/supervisor/update", @@ -77,7 +82,7 @@ export class HassioUpdate extends LitElement { )} ${this.hassOsInfo ? this._renderUpdateCard( - "HassOS", + "Operating System", this.hassOsInfo.version, this.hassOsInfo.version_latest, "hassio/hassos/update", @@ -149,13 +154,6 @@ export class HassioUpdate extends LitElement { haStyle, hassioStyle, css` - :host { - width: 33%; - } - paper-card { - display: inline-block; - margin-bottom: 32px; - } .icon { --iron-icon-height: 48px; --iron-icon-width: 48px; @@ -170,6 +168,10 @@ export class HassioUpdate extends LitElement { .warning { color: var(--secondary-text-color); } + .card-content { + height: calc(100% - 47px); + box-sizing: border-box; + } .card-actions { text-align: right; } diff --git a/hassio/src/entrypoint.ts b/hassio/src/entrypoint.ts index 13e57019fd..935d5676d7 100644 --- a/hassio/src/entrypoint.ts +++ b/hassio/src/entrypoint.ts @@ -1,9 +1,12 @@ window.loadES5Adapter().then(() => { + // eslint-disable-next-line + import(/* webpackChunkName: "roboto" */ "../../src/resources/roboto"); // eslint-disable-next-line import(/* webpackChunkName: "hassio-icons" */ "./resources/hassio-icons"); // eslint-disable-next-line import(/* webpackChunkName: "hassio-main" */ "./hassio-main"); }); + const styleEl = document.createElement("style"); styleEl.innerHTML = ` body { diff --git a/hassio/src/hassio-main.ts b/hassio/src/hassio-main.ts index 78954a6675..1b6ac36288 100644 --- a/hassio/src/hassio-main.ts +++ b/hassio/src/hassio-main.ts @@ -30,6 +30,10 @@ import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin"; // Don't codesplit it, that way the dashboard always loads fast. import "./hassio-pages-with-tabs"; import { navigate } from "../../src/common/navigate"; +import { + showAlertDialog, + AlertDialogParams, +} from "../../src/dialogs/generic/show-dialog-box"; // The register callback of the IronA11yKeysBehavior inside paper-icon-button // is not called, causing _keyBindings to be uninitiliazed for paper-icon-button, @@ -72,7 +76,6 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) { }, }, }; - @property() private _supervisorInfo: HassioSupervisorInfo; @property() private _hostInfo: HassioHostInfo; @property() private _hassOsInfo?: HassioHassOSInfo; @@ -81,7 +84,12 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) { protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); - applyThemesOnElement(this, this.hass.themes, this.hass.selectedTheme, true); + applyThemesOnElement( + this.parentElement, + this.hass.themes, + this.hass.selectedTheme, + true + ); this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev)); // Paulus - March 17, 2019 // We went to a single hass-toggle-menu event in HA 0.90. However, the @@ -107,6 +115,14 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) { }) ); + // Forward haptic events to parent window. + window.addEventListener("haptic", (ev) => { + // @ts-ignore + fireEvent(window.parent, ev.type, ev.detail, { + bubbles: false, + }); + }); + makeDialogManager(this, document.body); } @@ -158,31 +174,81 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) { } private async _redirectIngress(addonSlug: string) { + // When we trigger a navigation, we sleep to make sure we don't + // show the hassio dashboard before navigating away. + const awaitAlert = async ( + alertParams: AlertDialogParams, + action: () => void + ) => { + await new Promise((resolve) => { + alertParams.confirm = resolve; + showAlertDialog(this, alertParams); + }); + action(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + }; + + const createSessionPromise = createHassioSession(this.hass).then( + () => true, + () => false + ); + + let addon; + try { - const [addon] = await Promise.all([ - fetchHassioAddonInfo(this.hass, addonSlug).catch(() => { - throw new Error("Failed to fetch add-on info"); - }), - createHassioSession(this.hass).catch(() => { - throw new Error("Failed to create an ingress session"); - }), - ]); - if (!addon.ingress_url) { - alert("Add-on does not support Ingress"); - return; - } - if (addon.state !== "started") { - alert("Add-on is not running. Please start it first"); - navigate(this, `/hassio/addon/${addon.slug}`, true); - return; - } - location.assign(addon.ingress_url); - // await a promise that doesn't resolve, so we show the loading screen - // while we load the next page. - await new Promise(() => undefined); + addon = await fetchHassioAddonInfo(this.hass, addonSlug); } catch (err) { - alert("Unable to open ingress connection"); + await awaitAlert( + { + text: "Unable to fetch add-on info to start Ingress", + title: "Hass.io", + }, + () => history.back() + ); + + return; } + + if (!addon.ingress_url) { + await awaitAlert( + { + text: "Add-on does not support Ingress", + title: addon.name, + }, + () => history.back() + ); + + return; + } + + if (addon.state !== "started") { + await awaitAlert( + { + text: "Add-on is not running. Please start it first", + title: addon.name, + }, + () => navigate(this, `/hassio/addon/${addon.slug}`, true) + ); + + return; + } + + if (!(await createSessionPromise)) { + await awaitAlert( + { + text: "Unable to create an Ingress session", + title: addon.name, + }, + () => history.back() + ); + + return; + } + + location.assign(addon.ingress_url); + // await a promise that doesn't resolve, so we show the loading screen + // while we load the next page. + await new Promise(() => undefined); } private _apiCalled(ev) { diff --git a/hassio/src/hassio-pages-with-tabs.ts b/hassio/src/hassio-pages-with-tabs.ts index 9bb5eca5cd..becdccdf49 100644 --- a/hassio/src/hassio-pages-with-tabs.ts +++ b/hassio/src/hassio-pages-with-tabs.ts @@ -52,7 +52,7 @@ class HassioPagesWithTabs extends LitElement { .narrow=${this.narrow} hassio > -
Hass.io
+
Supervisor
${HAS_REFRESH_BUTTON.includes(page) ? html` - -`; - -document.head.appendChild(documentContainer.content); diff --git a/hassio/src/snapshots/hassio-snapshots.ts b/hassio/src/snapshots/hassio-snapshots.ts index db8d047cd7..5a96d2cd67 100644 --- a/hassio/src/snapshots/hassio-snapshots.ts +++ b/hassio/src/snapshots/hassio-snapshots.ts @@ -79,14 +79,14 @@ class HassioSnapshots extends LitElement { protected render(): TemplateResult { return html`
+

+ Create snapshot +

+

+ Snapshots allow you to easily backup and restore all data of your Home + Assistant instance. +

-
- Create snapshot -
- Snapshots allow you to easily backup and restore all data of your - Hass.io instance. -
-
+

Available snapshots

-
Available snapshots
${this._snapshots === undefined ? undefined : this._snapshots.length === 0 diff --git a/hassio/src/system/hassio-host-info.ts b/hassio/src/system/hassio-host-info.ts index b02dbf1b92..bdaaecbcfa 100644 --- a/hassio/src/system/hassio-host-info.ts +++ b/hassio/src/system/hassio-host-info.ts @@ -126,23 +126,13 @@ class HassioHostInfo extends LitElement { hassioStyle, css` paper-card { - display: inline-block; - width: 400px; - margin-left: 8px; + height: 100%; + width: 100%; } .card-content { - height: 200px; color: var(--primary-text-color); - } - @media screen and (max-width: 830px) { - paper-card { - margin-top: 8px; - margin-left: 0; - width: 100%; - } - .card-content { - height: auto; - } + box-sizing: border-box; + height: calc(100% - 47px); } .info { width: 100%; diff --git a/hassio/src/system/hassio-supervisor-info.ts b/hassio/src/system/hassio-supervisor-info.ts index 91854c0952..d375747e3d 100644 --- a/hassio/src/system/hassio-supervisor-info.ts +++ b/hassio/src/system/hassio-supervisor-info.ts @@ -32,7 +32,7 @@ class HassioSupervisorInfo extends LitElement { return html`
-

Hass.io supervisor

+

Supervisor

@@ -103,20 +103,13 @@ class HassioSupervisorInfo extends LitElement { hassioStyle, css` paper-card { - display: inline-block; - width: 400px; + height: 100%; + width: 100%; } .card-content { - height: 200px; color: var(--primary-text-color); - } - @media screen and (max-width: 830px) { - paper-card { - width: 100%; - } - .card-content { - height: auto; - } + box-sizing: border-box; + height: calc(100% - 47px); } .info { width: 100%; diff --git a/hassio/src/system/hassio-supervisor-log.ts b/hassio/src/system/hassio-supervisor-log.ts index 9524aac54c..a111969bb7 100644 --- a/hassio/src/system/hassio-supervisor-log.ts +++ b/hassio/src/system/hassio-supervisor-log.ts @@ -50,6 +50,9 @@ class HassioSupervisorLog extends LitElement { hassioStyle, ANSI_HTML_STYLE, css` + paper-card { + width: 100%; + } pre { white-space: pre-wrap; } diff --git a/hassio/src/system/hassio-system.ts b/hassio/src/system/hassio-system.ts index 5fbff5de08..e1ea7eb668 100644 --- a/hassio/src/system/hassio-system.ts +++ b/hassio/src/system/hassio-system.ts @@ -32,17 +32,19 @@ class HassioSystem extends LitElement { public render(): TemplateResult | void { return html`
-
Information
- - -
System log
+

Information

+
+ + +
+

System log

`; @@ -54,7 +56,7 @@ class HassioSystem extends LitElement { hassioStyle, css` .content { - margin: 4px; + margin: 8px; color: var(--primary-text-color); } .title { @@ -64,6 +66,9 @@ class HassioSystem extends LitElement { padding-left: 8px; margin-bottom: 8px; } + hassio-supervisor-log { + width: 100%; + } `, ]; } diff --git a/package.json b/package.json index 80a981ee46..9f1686c7b4 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@material/mwc-fab": "^0.10.0", "@material/mwc-ripple": "^0.10.0", "@material/mwc-switch": "^0.10.0", - "@mdi/svg": "4.8.95", + "@mdi/svg": "4.9.95", "@polymer/app-layout": "^3.0.2", "@polymer/app-localize-behavior": "^3.0.1", "@polymer/app-route": "^3.0.2", diff --git a/setup.py b/setup.py index 6ec0659e68..ea1d493ebf 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20200130.3", + version="20200212.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/common/datetime/check_options_support.ts b/src/common/datetime/check_options_support.ts new file mode 100644 index 0000000000..614f7f9828 --- /dev/null +++ b/src/common/datetime/check_options_support.ts @@ -0,0 +1,31 @@ +// Check for support of native locale string options +function checkToLocaleDateStringSupportsOptions() { + try { + new Date().toLocaleDateString("i"); + } catch (e) { + return e.name === "RangeError"; + } + return false; +} + +function checkToLocaleTimeStringSupportsOptions() { + try { + new Date().toLocaleTimeString("i"); + } catch (e) { + return e.name === "RangeError"; + } + return false; +} + +function checkToLocaleStringSupportsOptions() { + try { + new Date().toLocaleString("i"); + } catch (e) { + return e.name === "RangeError"; + } + return false; +} + +export const toLocaleDateStringSupportsOptions = checkToLocaleDateStringSupportsOptions(); +export const toLocaleTimeStringSupportsOptions = checkToLocaleTimeStringSupportsOptions(); +export const toLocaleStringSupportsOptions = checkToLocaleStringSupportsOptions(); diff --git a/src/common/datetime/format_date.ts b/src/common/datetime/format_date.ts index 813500ff4a..6a5e7e67f0 100644 --- a/src/common/datetime/format_date.ts +++ b/src/common/datetime/format_date.ts @@ -1,20 +1,11 @@ import fecha from "fecha"; +import { toLocaleDateStringSupportsOptions } from "./check_options_support"; -// Check for support of native locale string options -function toLocaleDateStringSupportsOptions() { - try { - new Date().toLocaleDateString("i"); - } catch (e) { - return e.name === "RangeError"; - } - return false; -} - -export default toLocaleDateStringSupportsOptions() +export const formatDate = toLocaleDateStringSupportsOptions ? (dateObj: Date, locales: string) => dateObj.toLocaleDateString(locales, { year: "numeric", month: "long", day: "numeric", }) - : (dateObj: Date) => fecha.format(dateObj, "mediumDate"); + : (dateObj: Date) => fecha.format(dateObj, "longDate"); diff --git a/src/common/datetime/format_date_time.ts b/src/common/datetime/format_date_time.ts index e8a02139d2..ee6d04f7dd 100644 --- a/src/common/datetime/format_date_time.ts +++ b/src/common/datetime/format_date_time.ts @@ -1,16 +1,7 @@ import fecha from "fecha"; +import { toLocaleStringSupportsOptions } from "./check_options_support"; -// Check for support of native locale string options -function toLocaleStringSupportsOptions() { - try { - new Date().toLocaleString("i"); - } catch (e) { - return e.name === "RangeError"; - } - return false; -} - -export default toLocaleStringSupportsOptions() +export const formatDateTime = toLocaleStringSupportsOptions ? (dateObj: Date, locales: string) => dateObj.toLocaleString(locales, { year: "numeric", @@ -19,4 +10,24 @@ export default toLocaleStringSupportsOptions() hour: "numeric", minute: "2-digit", }) - : (dateObj: Date) => fecha.format(dateObj, "haDateTime"); + : (dateObj: Date) => + fecha.format( + dateObj, + `${fecha.masks.longDate}, ${fecha.masks.shortTime}` + ); + +export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions + ? (dateObj: Date, locales: string) => + dateObj.toLocaleString(locales, { + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "2-digit", + second: "2-digit", + }) + : (dateObj: Date) => + fecha.format( + dateObj, + `${fecha.masks.longDate}, ${fecha.masks.mediumTime}` + ); diff --git a/src/common/datetime/format_time.ts b/src/common/datetime/format_time.ts index 2b747723a7..d8f0c8a8a0 100644 --- a/src/common/datetime/format_time.ts +++ b/src/common/datetime/format_time.ts @@ -1,19 +1,19 @@ import fecha from "fecha"; +import { toLocaleTimeStringSupportsOptions } from "./check_options_support"; -// Check for support of native locale string options -function toLocaleTimeStringSupportsOptions() { - try { - new Date().toLocaleTimeString("i"); - } catch (e) { - return e.name === "RangeError"; - } - return false; -} - -export default toLocaleTimeStringSupportsOptions() +export const formatTime = toLocaleTimeStringSupportsOptions ? (dateObj: Date, locales: string) => dateObj.toLocaleTimeString(locales, { hour: "numeric", minute: "2-digit", }) : (dateObj: Date) => fecha.format(dateObj, "shortTime"); + +export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions + ? (dateObj: Date, locales: string) => + dateObj.toLocaleTimeString(locales, { + hour: "numeric", + minute: "2-digit", + second: "2-digit", + }) + : (dateObj: Date) => fecha.format(dateObj, "mediumTime"); diff --git a/src/common/dom/apply_themes_on_element.ts b/src/common/dom/apply_themes_on_element.ts index 0e43f30bb1..12cd3757cf 100644 --- a/src/common/dom/apply_themes_on_element.ts +++ b/src/common/dom/apply_themes_on_element.ts @@ -1,3 +1,5 @@ +import { derivedStyles } from "../../resources/styles"; + const hexToRgb = (hex: string): string | null => { const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; const checkHex = hex.replace(shorthandRegex, (_m, r, g, b) => { @@ -36,7 +38,7 @@ export const applyThemesOnElement = ( } const styles = { ...element._themes }; if (themeName !== "default") { - const theme = themes.themes[themeName]; + const theme = { ...derivedStyles, ...themes.themes[themeName] }; Object.keys(theme).forEach((key) => { const prefixedKey = `--${key}`; element._themes[prefixedKey] = ""; diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index 62304f74e0..c802c7c56a 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -1,8 +1,8 @@ import { HassEntity } from "home-assistant-js-websocket"; import { computeStateDomain } from "./compute_state_domain"; -import formatDateTime from "../datetime/format_date_time"; -import formatDate from "../datetime/format_date"; -import formatTime from "../datetime/format_time"; +import { formatDateTime } from "../datetime/format_date_time"; +import { formatDate } from "../datetime/format_date"; +import { formatTime } from "../datetime/format_time"; import { LocalizeFunc } from "../translations/localize"; export const computeStateDisplay = ( diff --git a/src/common/style/icon_color_css.ts b/src/common/style/icon_color_css.ts index da89247533..565fd065e2 100644 --- a/src/common/style/icon_color_css.ts +++ b/src/common/style/icon_color_css.ts @@ -1,7 +1,6 @@ import { css } from "lit-element"; export const iconColorCSS = css` - ha-icon[data-domain="alarm_control_panel"][data-state="disarmed"], ha-icon[data-domain="alert"][data-state="on"], ha-icon[data-domain="automation"][data-state="on"], ha-icon[data-domain="binary_sensor"][data-state="on"], @@ -30,6 +29,34 @@ export const iconColorCSS = css` color: var(--heat-color, #ff8100); } + ha-icon[data-domain="alarm_control_panel"] { + color: var(--alarm-color-armed, var(--label-badge-red)); + } + + ha-icon[data-domain="alarm_control_panel"][data-state="disarmed"] { + color: var(--alarm-color-disarmed, var(--label-badge-green)); + } + + ha-icon[data-domain="alarm_control_panel"][data-state="pending"], + ha-icon[data-domain="alarm_control_panel"][data-state="arming"] { + color: var(--alarm-color-pending, var(--label-badge-yellow)); + animation: pulse 1s infinite; + } + + ha-icon[data-domain="alarm_control_panel"][data-state="triggered"] { + color: var(--alarm-color-triggered, var(--label-badge-red)); + animation: pulse 1s infinite; + } + + @keyframes pulse { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } + } + ha-icon[data-domain="plant"][data-state="problem"], ha-icon[data-domain="zwave"][data-state="dead"] { color: var(--error-state-color, #db4437); diff --git a/src/components/entity/ha-chart-base.js b/src/components/entity/ha-chart-base.js index 37eea60bea..ffa76a1d56 100644 --- a/src/components/entity/ha-chart-base.js +++ b/src/components/entity/ha-chart-base.js @@ -6,7 +6,7 @@ import { Debouncer } from "@polymer/polymer/lib/utils/debounce"; import { timeOut } from "@polymer/polymer/lib/utils/async"; import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class"; -import formatTime from "../../common/datetime/format_time"; +import { formatTime } from "../../common/datetime/format_time"; // eslint-disable-next-line no-unused-vars /* global Chart moment Color */ diff --git a/src/components/ha-switch.ts b/src/components/ha-switch.ts index a33f513bbb..89bae13454 100644 --- a/src/components/ha-switch.ts +++ b/src/components/ha-switch.ts @@ -1,15 +1,27 @@ -import { customElement, CSSResult, css, query, html } from "lit-element"; +import { + customElement, + CSSResult, + css, + query, + html, + property, +} from "lit-element"; import "@material/mwc-switch"; import { style } from "@material/mwc-switch/mwc-switch-css"; // tslint:disable-next-line import { Switch } from "@material/mwc-switch"; import { Constructor } from "../types"; +import { forwardHaptic } from "../data/haptics"; import { ripple } from "@material/mwc-ripple/ripple-directive"; // tslint:disable-next-line const MwcSwitch = customElements.get("mwc-switch") as Constructor; @customElement("ha-switch") export class HaSwitch extends MwcSwitch { + // Generate a haptic vibration. + // Only set to true if the new value of the switch is applied right away when toggling. + // Do not add haptic when a user is required to press save. + @property({ type: Boolean }) public haptic = false; @query("slot") private _slot!: HTMLSlotElement; protected firstUpdated() { @@ -22,6 +34,11 @@ export class HaSwitch extends MwcSwitch { "slotted", Boolean(this._slot.assignedNodes().length) ); + this.addEventListener("change", () => { + if (this.haptic) { + forwardHaptic("light"); + } + }); } protected render() { diff --git a/src/components/ha-yaml-editor.ts b/src/components/ha-yaml-editor.ts index a07297b365..a637a28cc3 100644 --- a/src/components/ha-yaml-editor.ts +++ b/src/components/ha-yaml-editor.ts @@ -21,9 +21,10 @@ const isEmpty = (obj: object) => { @customElement("ha-yaml-editor") export class HaYamlEditor extends LitElement { @property() public value?: any; + @property() public defaultValue?: any; @property() public isValid = true; @property() public label?: string; - @property() private _yaml?: string; + @property() private _yaml: string = ""; @query("ha-code-editor") private _editor?: HaCodeEditor; public setValue(value) { @@ -40,7 +41,9 @@ export class HaYamlEditor extends LitElement { } protected firstUpdated() { - this.setValue(this.value); + if (this.defaultValue) { + this.setValue(this.defaultValue); + } } protected render() { @@ -71,7 +74,6 @@ export class HaYamlEditor extends LitElement { if (value) { try { parsed = safeLoad(value); - isValid = true; } catch (err) { // Invalid YAML isValid = false; @@ -83,9 +85,7 @@ export class HaYamlEditor extends LitElement { this.value = parsed; this.isValid = isValid; - if (isValid) { - fireEvent(this, "value-changed", { value: parsed }); - } + fireEvent(this, "value-changed", { value: parsed, isValid } as any); } } diff --git a/src/components/state-history-chart-line.js b/src/components/state-history-chart-line.js index 6100e4022c..4456b285cc 100644 --- a/src/components/state-history-chart-line.js +++ b/src/components/state-history-chart-line.js @@ -5,7 +5,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element"; import "./entity/ha-chart-base"; import LocalizeMixin from "../mixins/localize-mixin"; -import formatDateTime from "../common/datetime/format_date_time"; +import { formatDateTimeWithSeconds } from "../common/datetime/format_date_time"; class StateHistoryChartLine extends LocalizeMixin(PolymerElement) { static get template() { @@ -317,7 +317,7 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) { const item = items[0]; const date = data.datasets[item.datasetIndex].data[item.index].x; - return formatDateTime(date, this.hass.language); + return formatDateTimeWithSeconds(date, this.hass.language); }; const chartOptions = { diff --git a/src/components/state-history-chart-timeline.js b/src/components/state-history-chart-timeline.js index ef0c91a87e..0ef2c6194c 100644 --- a/src/components/state-history-chart-timeline.js +++ b/src/components/state-history-chart-timeline.js @@ -6,7 +6,7 @@ import LocalizeMixin from "../mixins/localize-mixin"; import "./entity/ha-chart-base"; -import formatDateTime from "../common/datetime/format_date_time"; +import { formatDateTimeWithSeconds } from "../common/datetime/format_date_time"; import { computeRTL } from "../common/util/compute_rtl"; class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) { @@ -165,8 +165,8 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) { const formatTooltipLabel = (item, data) => { const values = data.datasets[item.datasetIndex].data[item.index]; - const start = formatDateTime(values[0], this.hass.language); - const end = formatDateTime(values[1], this.hass.language); + const start = formatDateTimeWithSeconds(values[0], this.hass.language); + const end = formatDateTimeWithSeconds(values[1], this.hass.language); const state = values[2]; return [state, start, end]; diff --git a/src/data/device_automation.ts b/src/data/device_automation.ts index bc58f1b00a..03c5a6870e 100644 --- a/src/data/device_automation.ts +++ b/src/data/device_automation.ts @@ -101,14 +101,16 @@ export const localizeDeviceAutomationAction = ( action: DeviceAction ) => { const state = action.entity_id ? hass.states[action.entity_id] : undefined; - return hass.localize( - `component.${action.domain}.device_automation.action_type.${action.type}`, - "entity_name", - state ? computeStateName(state) : "", - "subtype", + return ( hass.localize( - `component.${action.domain}.device_automation.action_subtype.${action.subtype}` - ) + `component.${action.domain}.device_automation.action_type.${action.type}`, + "entity_name", + state ? computeStateName(state) : action.entity_id || "", + "subtype", + hass.localize( + `component.${action.domain}.device_automation.action_subtype.${action.subtype}` + ) || action.subtype + ) || `"${action.subtype}" ${action.type}` ); }; @@ -119,14 +121,16 @@ export const localizeDeviceAutomationCondition = ( const state = condition.entity_id ? hass.states[condition.entity_id] : undefined; - return hass.localize( - `component.${condition.domain}.device_automation.condition_type.${condition.type}`, - "entity_name", - state ? computeStateName(state) : "", - "subtype", + return ( hass.localize( - `component.${condition.domain}.device_automation.condition_subtype.${condition.subtype}` - ) + `component.${condition.domain}.device_automation.condition_type.${condition.type}`, + "entity_name", + state ? computeStateName(state) : condition.entity_id || "", + "subtype", + hass.localize( + `component.${condition.domain}.device_automation.condition_subtype.${condition.subtype}` + ) || condition.subtype + ) || `"${condition.subtype}" ${condition.type}` ); }; @@ -135,13 +139,15 @@ export const localizeDeviceAutomationTrigger = ( trigger: DeviceTrigger ) => { const state = trigger.entity_id ? hass.states[trigger.entity_id] : undefined; - return hass.localize( - `component.${trigger.domain}.device_automation.trigger_type.${trigger.type}`, - "entity_name", - state ? computeStateName(state) : "", - "subtype", + return ( hass.localize( - `component.${trigger.domain}.device_automation.trigger_subtype.${trigger.subtype}` - ) + `component.${trigger.domain}.device_automation.trigger_type.${trigger.type}`, + "entity_name", + state ? computeStateName(state) : trigger.entity_id || "", + "subtype", + hass.localize( + `component.${trigger.domain}.device_automation.trigger_subtype.${trigger.subtype}` + ) || trigger.subtype + ) || `"${trigger.subtype}" ${trigger.type}` ); }; diff --git a/src/data/entity.ts b/src/data/entity.ts index 4960974c7e..f4d78f3175 100644 --- a/src/data/entity.ts +++ b/src/data/entity.ts @@ -1,4 +1,5 @@ export const UNAVAILABLE = "unavailable"; +export const UNKNOWN = "unknown"; export const ENTITY_COMPONENT_DOMAINS = [ "air_quality", diff --git a/src/data/hassio/hardware.ts b/src/data/hassio/hardware.ts index ee18581f2b..98a4b3ca57 100644 --- a/src/data/hassio/hardware.ts +++ b/src/data/hassio/hardware.ts @@ -2,12 +2,15 @@ import { HomeAssistant } from "../../types"; import { HassioResponse, hassioApiResultExtractor } from "./common"; export interface HassioHardwareAudioDevice { - device?: string; + device?: string | null; name: string; } interface HassioHardwareAudioList { - audio: { input: any; output: any }; + audio: { + input: { [key: string]: string }; + output: { [key: string]: string }; + }; } export interface HassioHardwareInfo { diff --git a/src/data/sensor.ts b/src/data/sensor.ts new file mode 100644 index 0000000000..e4da2f9825 --- /dev/null +++ b/src/data/sensor.ts @@ -0,0 +1 @@ +export const SENSOR_DEVICE_CLASS_BATTERY = "battery"; diff --git a/src/data/vacuum.ts b/src/data/vacuum.ts new file mode 100644 index 0000000000..0ad0738d1b --- /dev/null +++ b/src/data/vacuum.ts @@ -0,0 +1,22 @@ +import { + HassEntityAttributeBase, + HassEntityBase, +} from "home-assistant-js-websocket"; + +export const VACUUM_SUPPORT_PAUSE = 4; +export const VACUUM_SUPPORT_STOP = 8; +export const VACUUM_SUPPORT_RETURN_HOME = 16; +export const VACUUM_SUPPORT_FAN_SPEED = 32; +export const VACUUM_SUPPORT_BATTERY = 64; +export const VACUUM_SUPPORT_STATUS = 128; +export const VACUUM_SUPPORT_LOCATE = 512; +export const VACUUM_SUPPORT_CLEAN_SPOT = 1024; +export const VACUUM_SUPPORT_START = 8192; + +export type VacuumEntity = HassEntityBase & { + attributes: HassEntityAttributeBase & { + battery_level: number; + fan_speed: any; + [key: string]: any; + }; +}; diff --git a/src/data/ws-themes.ts b/src/data/ws-themes.ts index 13f37844d4..ed559e45c5 100644 --- a/src/data/ws-themes.ts +++ b/src/data/ws-themes.ts @@ -8,7 +8,7 @@ const fetchThemes = (conn) => const subscribeUpdates = (conn, store) => conn.subscribeEvents( - (event) => store.setState(event.data, true), + () => fetchThemes(conn).then((data) => store.setState(data, true)), "themes_updated" ); diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts index 76170c4b6c..8b09967cc7 100644 --- a/src/dialogs/config-flow/step-flow-pick-handler.ts +++ b/src/dialogs/config-flow/step-flow-pick-handler.ts @@ -39,7 +39,8 @@ class StepFlowPickHandler extends LitElement { private _getHandlers = memoizeOne((h: string[], filter?: string) => { const handlers: HandlerObj[] = h.map((handler) => { return { - name: this.hass.localize(`component.${handler}.config.title`), + name: + this.hass.localize(`component.${handler}.config.title`) || handler, slug: handler, }; }); diff --git a/src/dialogs/generic/show-dialog-box.ts b/src/dialogs/generic/show-dialog-box.ts index bd88af81b6..4d9975a6df 100644 --- a/src/dialogs/generic/show-dialog-box.ts +++ b/src/dialogs/generic/show-dialog-box.ts @@ -1,26 +1,32 @@ import { fireEvent } from "../../common/dom/fire_event"; -interface AlertDialogParams { +interface BaseDialogParams { confirmText?: string; text?: string; title?: string; - confirm?: (out?: string) => void; } -interface ConfirmationDialogParams extends AlertDialogParams { +export interface AlertDialogParams extends BaseDialogParams { + confirm?: () => void; +} + +export interface ConfirmationDialogParams extends BaseDialogParams { dismissText?: string; + confirm?: () => void; cancel?: () => void; } -interface PromptDialogParams extends AlertDialogParams { +export interface PromptDialogParams extends BaseDialogParams { inputLabel?: string; inputType?: string; defaultValue?: string; + confirm?: (out?: string) => void; } export interface DialogParams extends ConfirmationDialogParams, PromptDialogParams { + confirm?: (out?: string) => void; confirmation?: boolean; prompt?: boolean; } @@ -28,35 +34,57 @@ export interface DialogParams export const loadGenericDialog = () => import(/* webpackChunkName: "confirmation" */ "./dialog-box"); +const showDialogHelper = ( + element: HTMLElement, + dialogParams: DialogParams, + extra?: { + confirmation?: DialogParams["confirmation"]; + prompt?: DialogParams["prompt"]; + } +) => + new Promise((resolve) => { + const origCancel = dialogParams.cancel; + const origConfirm = dialogParams.confirm; + + fireEvent(element, "show-dialog", { + dialogTag: "dialog-box", + dialogImport: loadGenericDialog, + dialogParams: { + ...dialogParams, + ...extra, + cancel: () => { + resolve(extra?.prompt ? null : false); + if (origCancel) { + origCancel(); + } + }, + confirm: (out) => { + resolve(extra?.prompt ? out : true); + if (origConfirm) { + origConfirm(out); + } + }, + }, + }); + }); + export const showAlertDialog = ( element: HTMLElement, dialogParams: AlertDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-box", - dialogImport: loadGenericDialog, - dialogParams, - }); -}; +) => showDialogHelper(element, dialogParams); export const showConfirmationDialog = ( element: HTMLElement, dialogParams: ConfirmationDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-box", - dialogImport: loadGenericDialog, - dialogParams: { ...dialogParams, confirmation: true }, - }); -}; +) => + showDialogHelper(element, dialogParams, { confirmation: true }) as Promise< + boolean + >; export const showPromptDialog = ( element: HTMLElement, dialogParams: PromptDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-box", - dialogImport: loadGenericDialog, - dialogParams: { ...dialogParams, prompt: true }, - }); -}; +) => + showDialogHelper(element, dialogParams, { prompt: true }) as Promise< + null | string + >; diff --git a/src/dialogs/ha-more-info-dialog.js b/src/dialogs/ha-more-info-dialog.js index 3a34c1cfc9..878f17473d 100644 --- a/src/dialogs/ha-more-info-dialog.js +++ b/src/dialogs/ha-more-info-dialog.js @@ -80,7 +80,7 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) { class="no-padding" hass="[[hass]]" state-obj="[[stateObj]]" - dialog-element="[[_dialogElement]]" + dialog-element="[[_dialogElement()]]" registry-entry="[[_registryInfo]]" large="{{large}}" > @@ -102,7 +102,6 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) { observer: "_largeChanged", }, - _dialogElement: Object, _registryInfo: Object, dataDomain: { @@ -116,9 +115,8 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) { return ["_dialogOpenChanged(opened)"]; } - ready() { - super.ready(); - this._dialogElement = this; + _dialogElement() { + return this; } _computeDomain(stateObj) { diff --git a/src/dialogs/more-info/controls/more-info-climate.ts b/src/dialogs/more-info/controls/more-info-climate.ts index 7837536d14..bb1a4b2275 100644 --- a/src/dialogs/more-info/controls/more-info-climate.ts +++ b/src/dialogs/more-info/controls/more-info-climate.ts @@ -100,7 +100,8 @@ class MoreInfoClimate extends LitElement { ` : ""} - ${stateObj.attributes.temperature !== undefined + ${stateObj.attributes.temperature !== undefined && + stateObj.attributes.temperature !== null ? html` ` : ""} - ${stateObj.attributes.target_temp_low !== undefined || - stateObj.attributes.target_temp_high !== undefined + ${(stateObj.attributes.target_temp_low !== undefined && + stateObj.attributes.target_temp_low !== null) || + (stateObj.attributes.target_temp_high !== undefined && + stateObj.attributes.target_temp_high !== null) ? html` - - -
-
- Status: [[stateObj.attributes.status]] -
-
- - [[stateObj.attributes.battery_level]] % -
-
-
-

-
Vacuum cleaner commands:
-
- - - -
- -
-
- -
-
- -
-
- -
-
-
- -
-
- - - - - -
- - [[stateObj.attributes.fan_speed]] -
-
-

-
- - `; - } - - static get properties() { - return { - hass: { - type: Object, - }, - - inDialog: { - type: Boolean, - value: false, - }, - - stateObj: { - type: Object, - }, - }; - } - - supportsPause(stateObj) { - return supportsFeature(stateObj, 4); - } - - supportsStop(stateObj) { - return supportsFeature(stateObj, 8); - } - - supportsReturnHome(stateObj) { - return supportsFeature(stateObj, 16); - } - - supportsFanSpeed(stateObj) { - return supportsFeature(stateObj, 32); - } - - supportsBattery(stateObj) { - return supportsFeature(stateObj, 64); - } - - supportsStatus(stateObj) { - return supportsFeature(stateObj, 128); - } - - supportsLocate(stateObj) { - return supportsFeature(stateObj, 512); - } - - supportsCleanSpot(stateObj) { - return supportsFeature(stateObj, 1024); - } - - supportsStart(stateObj) { - return supportsFeature(stateObj, 8192); - } - - supportsCommandBar(stateObj) { - return ( - supportsFeature(stateObj, 4) | - supportsFeature(stateObj, 8) | - supportsFeature(stateObj, 16) | - supportsFeature(stateObj, 512) | - supportsFeature(stateObj, 1024) | - supportsFeature(stateObj, 8192) - ); - } - - fanSpeedChanged(ev) { - var oldVal = this.stateObj.attributes.fan_speed; - var newVal = ev.detail.value; - - if (!newVal || oldVal === newVal) return; - - this.hass.callService("vacuum", "set_fan_speed", { - entity_id: this.stateObj.entity_id, - fan_speed: newVal, - }); - } - - onStop() { - this.hass.callService("vacuum", "stop", { - entity_id: this.stateObj.entity_id, - }); - } - - onPlayPause() { - this.hass.callService("vacuum", "start_pause", { - entity_id: this.stateObj.entity_id, - }); - } - - onPause() { - this.hass.callService("vacuum", "pause", { - entity_id: this.stateObj.entity_id, - }); - } - - onStart() { - this.hass.callService("vacuum", "start", { - entity_id: this.stateObj.entity_id, - }); - } - - onLocate() { - this.hass.callService("vacuum", "locate", { - entity_id: this.stateObj.entity_id, - }); - } - - onCleanSpot() { - this.hass.callService("vacuum", "clean_spot", { - entity_id: this.stateObj.entity_id, - }); - } - - onReturnHome() { - this.hass.callService("vacuum", "return_to_base", { - entity_id: this.stateObj.entity_id, - }); - } -} - -customElements.define("more-info-vacuum", MoreInfoVacuum); diff --git a/src/dialogs/more-info/controls/more-info-vacuum.ts b/src/dialogs/more-info/controls/more-info-vacuum.ts new file mode 100644 index 0000000000..4d6d1c362f --- /dev/null +++ b/src/dialogs/more-info/controls/more-info-vacuum.ts @@ -0,0 +1,256 @@ +import "@polymer/iron-icon/iron-icon"; +import "@polymer/paper-icon-button/paper-icon-button"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-listbox/paper-listbox"; + +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; + +import { supportsFeature } from "../../../common/entity/supports-feature"; +import { HomeAssistant } from "../../../types"; + +import "../../../components/ha-paper-dropdown-menu"; +import "../../../components/ha-attributes"; +import { + VACUUM_SUPPORT_BATTERY, + VACUUM_SUPPORT_CLEAN_SPOT, + VACUUM_SUPPORT_FAN_SPEED, + VACUUM_SUPPORT_LOCATE, + VACUUM_SUPPORT_PAUSE, + VACUUM_SUPPORT_RETURN_HOME, + VACUUM_SUPPORT_START, + VACUUM_SUPPORT_STATUS, + VACUUM_SUPPORT_STOP, + VacuumEntity, +} from "../../../data/vacuum"; + +interface VacuumCommand { + translationKey: string; + icon: string; + serviceName: string; + isVisible: (stateObj: VacuumEntity) => boolean; +} + +const VACUUM_COMMANDS: VacuumCommand[] = [ + { + translationKey: "start", + icon: "hass:play", + serviceName: "start", + isVisible: (stateObj) => supportsFeature(stateObj, VACUUM_SUPPORT_START), + }, + { + translationKey: "pause", + icon: "hass:pause", + serviceName: "pause", + isVisible: (stateObj) => + // We need also to check if Start is supported because if not we show play-pause + supportsFeature(stateObj, VACUUM_SUPPORT_START) && + supportsFeature(stateObj, VACUUM_SUPPORT_PAUSE), + }, + { + translationKey: "start_pause", + icon: "hass:play-pause", + serviceName: "start_pause", + isVisible: (stateObj) => + // If start is supported, we don't show this button + !supportsFeature(stateObj, VACUUM_SUPPORT_START) && + supportsFeature(stateObj, VACUUM_SUPPORT_PAUSE), + }, + { + translationKey: "stop", + icon: "hass:stop", + serviceName: "stop", + isVisible: (stateObj) => supportsFeature(stateObj, VACUUM_SUPPORT_STOP), + }, + { + translationKey: "clean_spot", + icon: "hass:broom", + serviceName: "clean_spot", + isVisible: (stateObj) => + supportsFeature(stateObj, VACUUM_SUPPORT_CLEAN_SPOT), + }, + { + translationKey: "locate", + icon: "hass:map-marker", + serviceName: "locate", + isVisible: (stateObj) => supportsFeature(stateObj, VACUUM_SUPPORT_LOCATE), + }, + { + translationKey: "return_home", + icon: "hass:home-map-marker", + serviceName: "return_to_base", + isVisible: (stateObj) => + supportsFeature(stateObj, VACUUM_SUPPORT_RETURN_HOME), + }, +]; + +@customElement("more-info-vacuum") +class MoreInfoVacuum extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public stateObj?: VacuumEntity; + + protected render(): TemplateResult { + if (!this.hass || !this.stateObj) { + return html``; + } + + const stateObj = this.stateObj; + + const filterExtraAttributes = + "fan_speed,fan_speed_list,status,battery_level,battery_icon"; + + return html` +
+ ${supportsFeature(stateObj, VACUUM_SUPPORT_STATUS) + ? html` +
+ ${this.hass!.localize( + "ui.dialogs.more_info_control.vacuum.status" + )}: + + ${stateObj.attributes.status} +
+ ` + : ""} + ${supportsFeature(stateObj, VACUUM_SUPPORT_BATTERY) + ? html` + + + + ${stateObj.attributes.battery_level} % + +
` + : ""} + + + ${VACUUM_COMMANDS.some((item) => item.isVisible(stateObj)) + ? html` +
+

+
+ ${this.hass!.localize( + "ui.dialogs.more_info_control.vacuum.commands" + )} +
+
+ ${VACUUM_COMMANDS.filter((item) => + item.isVisible(stateObj) + ).map( + (item) => html` +
+ +
+ ` + )} +
+
+ ` + : ""} + ${supportsFeature(stateObj, VACUUM_SUPPORT_FAN_SPEED) + ? html` +
+
+ + + ${stateObj.attributes.fan_speed_list!.map( + (mode) => html` + + ${mode} + + ` + )} + + +
+ + + ${stateObj.attributes.fan_speed} + +
+
+

+
+ ` + : ""} + + + `; + } + + private callService(ev: CustomEvent) { + const entry = (ev.target! as any).entry as VacuumCommand; + this.hass.callService("vacuum", entry.serviceName, { + entity_id: this.stateObj!.entity_id, + }); + } + + private handleFanSpeedChanged(ev: CustomEvent) { + const oldVal = this.stateObj!.attributes.fan_speed; + const newVal = ev.detail.item.itemName; + + if (!newVal || oldVal === newVal) { + return; + } + + this.hass.callService("vacuum", "set_fan_speed", { + entity_id: this.stateObj!.entity_id, + fan_speed: newVal, + }); + } + + static get styles(): CSSResult { + return css` + :host { + @apply --paper-font-body1; + line-height: 1.5; + } + .status-subtitle { + color: var(--secondary-text-color); + } + paper-item { + cursor: pointer; + } + .flex-horizontal { + display: flex; + flex-direction: row; + justify-content: space-between; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "more-info-vacuum": MoreInfoVacuum; + } +} diff --git a/src/panels/config/automation/action/ha-automation-action-row.ts b/src/panels/config/automation/action/ha-automation-action-row.ts index f308d6d84a..43f756ae84 100644 --- a/src/panels/config/automation/action/ha-automation-action-row.ts +++ b/src/panels/config/automation/action/ha-automation-action-row.ts @@ -157,7 +157,7 @@ export default class HaAutomationActionRow extends LitElement { ` : ""} @@ -238,6 +238,9 @@ export default class HaAutomationActionRow extends LitElement { private _onYamlChange(ev: CustomEvent) { ev.stopPropagation(); + if (!ev.detail.isValid) { + return; + } fireEvent(this, "value-changed", { value: ev.detail.value }); } diff --git a/src/panels/config/automation/action/types/ha-automation-action-event.ts b/src/panels/config/automation/action/types/ha-automation-action-event.ts index e66fa62e8b..115b11680b 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-event.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-event.ts @@ -57,13 +57,17 @@ export class HaEventAction extends LitElement implements ActionElement { "ui.panel.config.automation.editor.actions.type.event.service_data" )} .name=${"event_data"} - .value=${event_data} + .defaultValue=${event_data} @value-changed=${this._dataChanged} > `; } private _dataChanged(ev: CustomEvent): void { + ev.stopPropagation(); + if (!ev.detail.isValid) { + return; + } this._actionData = ev.detail.value; handleChangeEvent(this, ev); } diff --git a/src/panels/config/automation/action/types/ha-automation-action-service.ts b/src/panels/config/automation/action/types/ha-automation-action-service.ts index cd59512da6..a0ac5a6ebe 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-service.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-service.ts @@ -94,13 +94,17 @@ export class HaServiceAction extends LitElement implements ActionElement { "ui.panel.config.automation.editor.actions.type.service.service_data" )} .name=${"data"} - .value=${data} + .defaultValue=${data} @value-changed=${this._dataChanged} > `; } private _dataChanged(ev: CustomEvent): void { + ev.stopPropagation(); + if (!ev.detail.isValid) { + return; + } this._actionData = ev.detail.value; handleChangeEvent(this, ev); } diff --git a/src/panels/config/automation/condition/ha-automation-condition-editor.ts b/src/panels/config/automation/condition/ha-automation-condition-editor.ts index 57a9726bc3..4082b68265 100644 --- a/src/panels/config/automation/condition/ha-automation-condition-editor.ts +++ b/src/panels/config/automation/condition/ha-automation-condition-editor.ts @@ -54,7 +54,7 @@ export default class HaAutomationConditionEditor extends LitElement { ` : ""} @@ -114,6 +114,9 @@ export default class HaAutomationConditionEditor extends LitElement { private _onYamlChange(ev: CustomEvent) { ev.stopPropagation(); + if (!ev.detail.isValid) { + return; + } fireEvent(this, "value-changed", { value: ev.detail.value }); } } diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index 608956c487..2de58ea69b 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -28,7 +28,7 @@ import { showAutomationEditor, AutomationConfig, } from "../../../data/automation"; -import format_date_time from "../../../common/datetime/format_date_time"; +import { formatDateTime } from "../../../common/datetime/format_date_time"; import { fireEvent } from "../../../common/dom/fire_event"; import { showThingtalkDialog } from "./show-dialog-thingtalk"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; @@ -102,7 +102,7 @@ class HaAutomationPicker extends LitElement { "ui.card.automation.last_triggered" )}: ${ automation.attributes.last_triggered - ? format_date_time( + ? formatDateTime( new Date(automation.attributes.last_triggered), this.hass.language ) diff --git a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts index cb5f6bc2a9..3559ca2053 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts @@ -139,7 +139,7 @@ export default class HaAutomationTriggerRow extends LitElement { ` : ""} @@ -213,6 +213,9 @@ export default class HaAutomationTriggerRow extends LitElement { private _onYamlChange(ev: CustomEvent) { ev.stopPropagation(); + if (!ev.detail.isValid) { + return; + } fireEvent(this, "value-changed", { value: ev.detail.value }); } diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-event.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-event.ts index 7a70ba055a..ef163d50d2 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-event.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-event.ts @@ -35,13 +35,17 @@ export class HaEventTrigger extends LitElement implements TriggerElement { "ui.panel.config.automation.editor.triggers.type.event.event_data" )} .name=${"event_data"} - .value=${event_data} + .defaultValue=${event_data} @value-changed=${this._valueChanged} > `; } private _valueChanged(ev: CustomEvent): void { + ev.stopPropagation(); + if (!ev.detail.isValid) { + return; + } handleChangeEvent(this, ev); } } diff --git a/src/panels/config/cloud/account/cloud-account.js b/src/panels/config/cloud/account/cloud-account.js index 19f37a5aff..6f3cacefa5 100644 --- a/src/panels/config/cloud/account/cloud-account.js +++ b/src/panels/config/cloud/account/cloud-account.js @@ -16,7 +16,7 @@ import "./cloud-remote-pref"; import { EventsMixin } from "../../../../mixins/events-mixin"; import { fetchCloudSubscriptionInfo } from "../../../../data/cloud"; -import formatDateTime from "../../../../common/datetime/format_date_time"; +import { formatDateTime } from "../../../../common/datetime/format_date_time"; import LocalizeMixin from "../../../../mixins/localize-mixin"; /* diff --git a/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts b/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts index 2964721dbf..89df7f4812 100644 --- a/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts +++ b/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts @@ -16,7 +16,7 @@ import { HaPaperDialog } from "../../../../components/dialog/ha-paper-dialog"; import { HomeAssistant } from "../../../../types"; import { haStyle } from "../../../../resources/styles"; import { CloudCertificateParams as CloudCertificateDialogParams } from "./show-dialog-cloud-certificate"; -import format_date_time from "../../../../common/datetime/format_date_time"; +import { formatDateTime } from "../../../../common/datetime/format_date_time"; @customElement("dialog-cloud-certificate") class DialogCloudCertificate extends LitElement { @@ -50,7 +50,7 @@ class DialogCloudCertificate extends LitElement { ${this.hass!.localize( "ui.panel.config.cloud.dialog_certificate.certificate_expiration_date" )} - ${format_date_time( + ${formatDateTime( new Date(certificateInfo.expire_date), this.hass!.language )}
diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index a0090ff6a4..ac5ac1a30d 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -177,26 +177,27 @@ export class EntityRegistrySettings extends LitElement { } } - private async _deleteEntry(): Promise { + private async _confirmDeleteEntry(): Promise { + if ( + !(await showConfirmationDialog(this, { + text: this.hass.localize( + "ui.dialogs.entity_registry.editor.confirm_delete" + ), + })) + ) { + return; + } + this._submitting = true; try { - await removeEntityRegistryEntry(this.hass!, this._entityId); + await removeEntityRegistryEntry(this.hass!, this._origEntityId); fireEvent(this as HTMLElement, "close-dialog"); } finally { this._submitting = false; } } - private _confirmDeleteEntry(): void { - showConfirmationDialog(this, { - text: this.hass.localize( - "ui.dialogs.entity_registry.editor.confirm_delete" - ), - confirm: () => this._deleteEntry(), - }); - } - private _disabledByChanged(ev: Event): void { this._disabledBy = (ev.target as HaSwitch).checked ? null : "user"; } diff --git a/src/panels/developer-tools/event/event-subscribe-card.ts b/src/panels/developer-tools/event/event-subscribe-card.ts index 7b89c464f4..4816672d59 100644 --- a/src/panels/developer-tools/event/event-subscribe-card.ts +++ b/src/panels/developer-tools/event/event-subscribe-card.ts @@ -13,7 +13,7 @@ import { HassEvent } from "home-assistant-js-websocket"; import { HomeAssistant } from "../../../types"; import { PolymerChangedEvent } from "../../../polymer-types"; import "../../../components/ha-card"; -import format_time from "../../../common/datetime/format_time"; +import { formatTime } from "../../../common/datetime/format_time"; @customElement("event-subscribe-card") class EventSubscribeCard extends LitElement { @@ -78,7 +78,7 @@ class EventSubscribeCard extends LitElement { "name", ev.id )} - ${format_time( + ${formatTime( new Date(ev.event.time_fired), this.hass!.language )}: diff --git a/src/panels/developer-tools/info/developer-tools-info.ts b/src/panels/developer-tools/info/developer-tools-info.ts index 7141d4b132..dc9b45ec53 100644 --- a/src/panels/developer-tools/info/developer-tools-info.ts +++ b/src/panels/developer-tools/info/developer-tools-info.ts @@ -11,6 +11,7 @@ import { HomeAssistant } from "../../../types"; import { haStyle } from "../../../resources/styles"; import "./system-health-card"; +import "./integrations-card"; const JS_TYPE = __BUILD__; const JS_VERSION = __VERSION__; @@ -149,6 +150,7 @@ class HaPanelDevInfo extends LitElement {
+
`; } @@ -205,7 +207,8 @@ class HaPanelDevInfo extends LitElement { color: var(--dark-primary-color); } - system-health-card { + system-health-card, + integrations-card { display: block; max-width: 600px; margin: 0 auto; diff --git a/src/panels/developer-tools/info/integrations-card.ts b/src/panels/developer-tools/info/integrations-card.ts new file mode 100644 index 0000000000..e51f73366a --- /dev/null +++ b/src/panels/developer-tools/info/integrations-card.ts @@ -0,0 +1,75 @@ +import { + LitElement, + property, + TemplateResult, + html, + customElement, + CSSResult, + css, +} from "lit-element"; +import { HomeAssistant } from "../../../types"; +import memoizeOne from "memoize-one"; + +@customElement("integrations-card") +class IntegrationsCard extends LitElement { + @property() public hass!: HomeAssistant; + + private _sortedIntegrations = memoizeOne((components: string[]) => { + return components.filter((comp) => !comp.includes(".")).sort(); + }); + + protected render(): TemplateResult { + return html` + +
+ + ${this._sortedIntegrations(this.hass!.config.components).map( + (domain) => html` + + + + + + ` + )} + +
${domain} + + Documentation + + + + Issues + +
+ + `; + } + + static get styles(): CSSResult { + return css` + td { + line-height: 2em; + padding: 0 8px; + } + td:first-child { + padding-left: 0; + } + a { + color: var(--primary-color); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "integrations-card": IntegrationsCard; + } +} diff --git a/src/panels/developer-tools/info/system-health-card.ts b/src/panels/developer-tools/info/system-health-card.ts index 35e1976d9e..92bef82cf6 100644 --- a/src/panels/developer-tools/info/system-health-card.ts +++ b/src/panels/developer-tools/info/system-health-card.ts @@ -78,7 +78,7 @@ class SystemHealthCard extends LitElement { } return html` - +
${sections}
`; diff --git a/src/panels/developer-tools/logs/system-log-card.ts b/src/panels/developer-tools/logs/system-log-card.ts index ab0f303ca5..1a03a72e1c 100644 --- a/src/panels/developer-tools/logs/system-log-card.ts +++ b/src/panels/developer-tools/logs/system-log-card.ts @@ -16,8 +16,8 @@ import "../../../components/buttons/ha-call-service-button"; import "../../../components/buttons/ha-progress-button"; import { HomeAssistant } from "../../../types"; import { LoggedError, fetchSystemLog } from "../../../data/system_log"; -import formatDateTime from "../../../common/datetime/format_date_time"; -import formatTime from "../../../common/datetime/format_time"; +import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time"; +import { formatTimeWithSeconds } from "../../../common/datetime/format_time"; import { showSystemLogDetailDialog } from "./show-dialog-system-log-detail"; const formatLogTime = (date, language: string) => { @@ -26,8 +26,8 @@ const formatLogTime = (date, language: string) => { const dateTimeDay = new Date(date * 1000).setHours(0, 0, 0, 0); return dateTimeDay < today - ? formatDateTime(dateTime, language) - : formatTime(dateTime, language); + ? formatDateTimeWithSeconds(dateTime, language) + : formatTimeWithSeconds(dateTime, language); }; @customElement("system-log-card") diff --git a/src/panels/developer-tools/mqtt/developer-tools-mqtt.ts b/src/panels/developer-tools/mqtt/developer-tools-mqtt.ts index aaef686ffc..e527f4d0dc 100644 --- a/src/panels/developer-tools/mqtt/developer-tools-mqtt.ts +++ b/src/panels/developer-tools/mqtt/developer-tools-mqtt.ts @@ -120,10 +120,6 @@ class HaPanelDevMqtt extends LitElement { direction: ltr; } - mwc-button { - background-color: white; - } - mqtt-subscribe-card { display: block; margin: 16px auto; diff --git a/src/panels/developer-tools/mqtt/mqtt-subscribe-card.ts b/src/panels/developer-tools/mqtt/mqtt-subscribe-card.ts index 24b40c1601..67d128ff7a 100644 --- a/src/panels/developer-tools/mqtt/mqtt-subscribe-card.ts +++ b/src/panels/developer-tools/mqtt/mqtt-subscribe-card.ts @@ -11,7 +11,7 @@ import "@material/mwc-button"; import "@polymer/paper-input/paper-input"; import { HomeAssistant } from "../../../types"; import "../../../components/ha-card"; -import format_time from "../../../common/datetime/format_time"; +import { formatTime } from "../../../common/datetime/format_time"; import { subscribeMQTTTopic, MQTTMessage } from "../../../data/mqtt"; @@ -85,7 +85,7 @@ class MqttSubscribeCard extends LitElement { "topic", msg.message.topic, "time", - format_time(msg.time, this.hass!.language) + formatTime(msg.time, this.hass!.language) )}
${msg.payload}
diff --git a/src/panels/history/ha-panel-history.js b/src/panels/history/ha-panel-history.js index 3daf0f4345..fcee278b61 100644 --- a/src/panels/history/ha-panel-history.js +++ b/src/panels/history/ha-panel-history.js @@ -16,7 +16,7 @@ import "../../data/ha-state-history-data"; import "../../resources/ha-date-picker-style"; import "../../resources/ha-style"; -import formatDate from "../../common/datetime/format_date"; +import { formatDate } from "../../common/datetime/format_date"; import LocalizeMixin from "../../mixins/localize-mixin"; import { computeRTL } from "../../common/util/compute_rtl"; diff --git a/src/panels/logbook/ha-logbook-data.js b/src/panels/logbook/ha-logbook-data.js index 8be479e4f8..12a1969f4f 100644 --- a/src/panels/logbook/ha-logbook-data.js +++ b/src/panels/logbook/ha-logbook-data.js @@ -59,7 +59,7 @@ class HaLogbookData extends PolymerElement { this._setIsLoading(true); - this.getDate(this.filterDate, this.filterPeriod, this.filterEntity).then( + this.getData(this.filterDate, this.filterPeriod, this.filterEntity).then( (logbookEntries) => { this._setEntries(logbookEntries); this._setIsLoading(false); @@ -67,7 +67,7 @@ class HaLogbookData extends PolymerElement { ); } - getDate(date, period, entityId) { + getData(date, period, entityId) { if (!entityId) entityId = ALL_ENTITIES; if (!DATA_CACHE[period]) DATA_CACHE[period] = []; diff --git a/src/panels/logbook/ha-logbook.ts b/src/panels/logbook/ha-logbook.ts index 86a0f8455a..f72edab10c 100644 --- a/src/panels/logbook/ha-logbook.ts +++ b/src/panels/logbook/ha-logbook.ts @@ -1,6 +1,6 @@ import "../../components/ha-icon"; -import formatTime from "../../common/datetime/format_time"; -import formatDate from "../../common/datetime/format_date"; +import { formatTimeWithSeconds } from "../../common/datetime/format_time"; +import { formatDate } from "../../common/datetime/format_date"; import { domainIcon } from "../../common/entity/domain_icon"; import { stateIcon } from "../../common/entity/state_icon"; import { computeRTL } from "../../common/util/compute_rtl"; @@ -25,19 +25,14 @@ class HaLogbook extends LitElement { // @ts-ignore private _rtl = false; - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - if (!changedProps.has("hass")) { - return; - } + protected shouldUpdate(changedProps: PropertyValues) { const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if (oldHass && oldHass.language !== this.hass.language) { - this._rtl = computeRTL(this.hass); - } + const languageChanged = + oldHass === undefined || oldHass.language !== this.hass.language; + return changedProps.has("entries") || languageChanged; } - protected firstUpdated(changedProps: PropertyValues) { - super.firstUpdated(changedProps); + protected updated(_changedProps: PropertyValues) { this._rtl = computeRTL(this.hass); } @@ -80,7 +75,7 @@ class HaLogbook extends LitElement {
- ${formatTime(new Date(item.when), this.hass.language)} + ${formatTimeWithSeconds(new Date(item.when), this.hass.language)}
- ${entityConf.show_last_changed + ${computeDomain(entityConf.entity) === "sensor" && + stateObj.attributes.device_class === "timestamp" && + stateObj.state !== UNAVAILABLE && + stateObj.state !== UNKNOWN + ? html` + + ` + : entityConf.show_last_changed ? relativeTime( new Date(stateObj.last_changed), this.hass!.localize diff --git a/src/panels/lovelace/cards/hui-legacy-wrapper-card.js b/src/panels/lovelace/cards/hui-legacy-wrapper-card.js deleted file mode 100644 index 0bc35371bd..0000000000 --- a/src/panels/lovelace/cards/hui-legacy-wrapper-card.js +++ /dev/null @@ -1,58 +0,0 @@ -import { createErrorCardConfig } from "./hui-error-card"; -import { computeDomain } from "../../../common/entity/compute_domain"; - -export default class LegacyWrapperCard extends HTMLElement { - constructor(tag, domain) { - super(); - this._tag = tag.toUpperCase(); - this._domain = domain; - this._element = null; - } - - getCardSize() { - return 3; - } - - setConfig(config) { - if (!config.entity) { - throw new Error("No entity specified"); - } - - if (computeDomain(config.entity) !== this._domain) { - throw new Error( - `Specified entity needs to be of domain ${this._domain}.` - ); - } - - this._config = config; - } - - set hass(hass) { - const entityId = this._config.entity; - - if (entityId in hass.states) { - this._ensureElement(this._tag); - this.lastChild.hass = hass; - this.lastChild.stateObj = hass.states[entityId]; - this.lastChild.config = this._config; - } else { - this._ensureElement("HUI-ERROR-CARD"); - this.lastChild.setConfig( - createErrorCardConfig( - `No state available for ${entityId}`, - this._config - ) - ); - } - } - - _ensureElement(tag) { - if (this.lastChild && this.lastChild.tagName === tag) return; - - if (this.lastChild) { - this.removeChild(this.lastChild); - } - - this.appendChild(document.createElement(tag)); - } -} diff --git a/src/panels/lovelace/cards/hui-media-control-card.js b/src/panels/lovelace/cards/hui-media-control-card.js deleted file mode 100644 index bc133b41a2..0000000000 --- a/src/panels/lovelace/cards/hui-media-control-card.js +++ /dev/null @@ -1,22 +0,0 @@ -import "../../../cards/ha-media_player-card"; - -import LegacyWrapperCard from "./hui-legacy-wrapper-card"; - -class HuiMediaControlCard extends LegacyWrapperCard { - static async getConfigElement() { - await import( - /* webpackChunkName: "hui-media-control-card-editor" */ "../editor/config-elements/hui-media-control-card-editor" - ); - return document.createElement("hui-media-control-card-editor"); - } - - static getStubConfig() { - return { entity: "" }; - } - - constructor() { - super("ha-media_player-card", "media_player"); - } -} - -customElements.define("hui-media-control-card", HuiMediaControlCard); diff --git a/src/panels/lovelace/cards/hui-media-control-card.ts b/src/panels/lovelace/cards/hui-media-control-card.ts new file mode 100644 index 0000000000..c5035a9c58 --- /dev/null +++ b/src/panels/lovelace/cards/hui-media-control-card.ts @@ -0,0 +1,306 @@ +import { + html, + LitElement, + PropertyValues, + TemplateResult, + customElement, + property, + css, + CSSResult, +} from "lit-element"; +import { classMap } from "lit-html/directives/class-map"; +import { HassEntity } from "home-assistant-js-websocket"; +import "@polymer/paper-icon-button/paper-icon-button"; + +import "../../../components/ha-card"; +import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; +import { computeStateName } from "../../../common/entity/compute_state_name"; +import { supportsFeature } from "../../../common/entity/supports-feature"; +import { OFF_STATES, SUPPORT_PAUSE } from "../../../data/media-player"; +import { hasConfigOrEntityChanged } from "../common/has-changed"; +import { HomeAssistant, MediaEntity } from "../../../types"; +import { LovelaceCard, LovelaceCardEditor } from "../types"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { MediaControlCardConfig } from "./types"; + +@customElement("hui-media-control-card") +export class HuiMediaControlCard extends LitElement implements LovelaceCard { + public static async getConfigElement(): Promise { + await import( + /* webpackChunkName: "hui-media-control-card-editor" */ "../editor/config-elements/hui-media-control-card-editor" + ); + return document.createElement("hui-media-control-card-editor"); + } + + public static getStubConfig(): object { + return { entity: "" }; + } + + @property() public hass?: HomeAssistant; + @property() private _config?: MediaControlCardConfig; + + public getCardSize(): number { + return 3; + } + + public setConfig(config: MediaControlCardConfig): void { + if (!config.entity || config.entity.split(".")[0] !== "media_player") { + throw new Error("Specify an entity from within the media_player domain."); + } + + this._config = { theme: "default", ...config }; + } + + protected render(): TemplateResult { + if (!this.hass || !this._config) { + return html``; + } + const stateObj = this.hass.states[this._config.entity] as MediaEntity; + + if (!stateObj) { + return html` + ${this.hass.localize( + "ui.panel.lovelace.warning.entity_not_found", + "entity", + this._config.entity + )} + `; + } + const image = + stateObj.attributes.entity_picture || + "../static/images/card_media_player_bg.png"; + + return html` + +
+
+
+ ${this._config!.name || + computeStateName(this.hass!.states[this._config!.entity])} +
+ ${this._computeMediaTitle(stateObj)} +
+
+
+ ${OFF_STATES.includes(stateObj.state) + ? "" + : html` + + `} +
+ +
+ + + +
+ +
+
+ `; + } + + protected shouldUpdate(changedProps: PropertyValues): boolean { + return hasConfigOrEntityChanged(this, changedProps); + } + + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + if (!this._config || !this.hass || !changedProps.has("hass")) { + return; + } + + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + const oldConfig = changedProps.get("_config") as + | MediaControlCardConfig + | undefined; + + if ( + !oldHass || + !oldConfig || + oldHass.themes !== this.hass.themes || + oldConfig.theme !== this._config.theme + ) { + applyThemesOnElement(this, this.hass.themes, this._config.theme); + } + } + + private _computeMediaTitle(stateObj: HassEntity): string { + let prefix; + let suffix; + + switch (stateObj.attributes.media_content_type) { + case "music": + prefix = stateObj.attributes.media_artist; + suffix = stateObj.attributes.media_title; + break; + case "tvshow": + prefix = stateObj.attributes.media_series_title; + suffix = stateObj.attributes.media_title; + break; + default: + prefix = + stateObj.attributes.media_title || + stateObj.attributes.app_name || + this.hass!.localize(`state.media_player.${stateObj.state}`) || + this.hass!.localize(`state.default.${stateObj.state}`) || + stateObj.state; + suffix = ""; + } + + return prefix && suffix ? `${prefix}: ${suffix}` : prefix || suffix || ""; + } + + private _handleMoreInfo() { + fireEvent(this, "hass-more-info", { + entityId: this._config!.entity, + }); + } + + private _handleClick(e: MouseEvent) { + this.hass!.callService("media_player", (e.currentTarget! as any).action, { + entity_id: this._config!.entity, + }); + } + + static get styles(): CSSResult { + return css` + .ratio { + position: relative; + width: 100%; + height: 0; + padding-bottom: 56.25%; + } + + .image { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: block; + transition: filter 0.2s linear; + background-position: center center; + background-size: cover; + } + + .no-image { + padding-bottom: 88px; + } + + .no-image > .image { + background-position: center center; + background-repeat: no-repeat; + background-color: var(--primary-color); + background-size: initial; + } + + .no-image > .caption { + background-color: initial; + box-sizing: border-box; + height: 88px; + } + + .controls { + padding: 8px; + display: flex; + justify-content: space-between; + align-items: center; + } + + .controls > div { + display: flex; + align-items: center; + } + + .controls paper-icon-button { + width: 44px; + height: 44px; + } + paper-icon-button { + opacity: var(--dark-primary-opacity); + } + + paper-icon-button[disabled] { + opacity: var(--dark-disabled-opacity); + } + + .playPauseButton { + width: 56px !important; + height: 56px !important; + background-color: var(--primary-color); + color: white; + border-radius: 50%; + padding: 8px; + transition: background-color 0.5s; + } + + .caption { + position: absolute; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, var(--dark-secondary-opacity)); + padding: 8px 16px; + font-size: 14px; + font-weight: 500; + color: white; + transition: background-color 0.5s; + } + + .title { + font-size: 1.2em; + margin: 8px 0 4px; + } + + .progress { + width: 100%; + height: var(--paper-progress-height, 4px); + margin-top: calc(-1 * var(--paper-progress-height, 4px)); + --paper-progress-active-color: var(--accent-color); + --paper-progress-container-color: rgba(200, 200, 200, 0.5); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-media-control-card": HuiMediaControlCard; + } +} diff --git a/src/panels/lovelace/cards/hui-sensor-card.ts b/src/panels/lovelace/cards/hui-sensor-card.ts index 9c5e693466..3c6656bd04 100644 --- a/src/panels/lovelace/cards/hui-sensor-card.ts +++ b/src/panels/lovelace/cards/hui-sensor-card.ts @@ -27,6 +27,17 @@ import { SensorCardConfig } from "./types"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import { actionHandler } from "../common/directives/action-handler-directive"; +const average = (items): number => { + return ( + items.reduce((sum, entry) => sum + parseFloat(entry.state), 0) / + items.length + ); +}; + +const lastValue = (items): number => { + return parseFloat(items[items.length - 1].state) || 0; +}; + const midPoint = ( _Ax: number, _Ay: number, @@ -69,34 +80,40 @@ const calcPoints = ( max: number ): number[][] => { const coords = [] as number[][]; - const margin = 5; const height = 80; - width -= 10; let yRatio = (max - min) / height; yRatio = yRatio !== 0 ? yRatio : height; let xRatio = width / (hours - (detail === 1 ? 1 : 0)); xRatio = isFinite(xRatio) ? xRatio : width; + + const first = history.filter(Boolean)[0]; + let last = [average(first), lastValue(first)]; + const getCoords = (item, i, offset = 0, depth = 1) => { if (depth > 1) { return item.forEach((subItem, index) => getCoords(subItem, i, index, depth - 1) ); } - const average = - item.reduce((sum, entry) => sum + parseFloat(entry.state), 0) / - item.length; - const x = xRatio * (i + offset / 6) + margin; - const y = height - (average - min) / yRatio + margin * 2; + const x = xRatio * (i + offset / 6); + + if (item) { + last = [average(item), lastValue(item)]; + } + const y = height - ((item ? last[0] : last[1]) - min) / yRatio; return coords.push([x, y]); }; - history.forEach((item, i) => getCoords(item, i, 0, detail)); - if (coords.length === 1) { - coords[1] = [width + margin, coords[0][1]]; + for (let i = 0; i < history.length; i += 1) { + getCoords(history[i], i, 0, detail); } - coords.push([width + margin, coords[coords.length - 1][1]]); + if (coords.length === 1) { + coords[1] = [width, coords[0][1]]; + } + + coords.push([width, coords[coords.length - 1][1]]); return coords; }; @@ -227,14 +244,27 @@ class HuiSensorCard extends LitElement implements LovelaceCard { } else { graph = svg` - + + + + + + + + + + `; } @@ -247,17 +277,15 @@ class HuiSensorCard extends LitElement implements LovelaceCard { .actionHandler=${actionHandler()} tabindex="0" > -
+
+
+ ${this._config.name || computeStateName(stateObj)} +
-
- ${this._config.name || computeStateName(stateObj)} -
${stateObj.state} @@ -285,7 +313,7 @@ class HuiSensorCard extends LitElement implements LovelaceCard { protected updated(changedProps: PropertyValues) { super.updated(changedProps); - if (!this._config || this._config.graph !== "line" || !this.hass) { + if (!this._config || !this.hass) { return; } @@ -303,11 +331,13 @@ class HuiSensorCard extends LitElement implements LovelaceCard { applyThemesOnElement(this, this.hass.themes, this._config!.theme); } - const minute = 60000; - if (changedProps.has("_config")) { - this._getHistory(); - } else if (Date.now() - this._date!.getTime() >= minute) { - this._getHistory(); + if (this._config.graph === "line") { + const minute = 60000; + if (changedProps.has("_config")) { + this._getHistory(); + } else if (Date.now() - this._date!.getTime() >= minute) { + this._getHistory(); + } } } @@ -353,9 +383,9 @@ class HuiSensorCard extends LitElement implements LovelaceCard { display: flex; flex-direction: column; flex: 1; - padding: 16px; position: relative; cursor: pointer; + overflow: hidden; } ha-card:focus { @@ -368,6 +398,11 @@ class HuiSensorCard extends LitElement implements LovelaceCard { } .header { + margin: 16px 16px 0; + justify-content: space-between; + } + + .name { align-items: center; display: flex; min-width: 0; @@ -375,13 +410,13 @@ class HuiSensorCard extends LitElement implements LovelaceCard { position: relative; } - .name { + .name > span { display: block; display: -webkit-box; font-size: 1.2rem; font-weight: 500; max-height: 1.4rem; - margin-top: 2px; + top: 2px; opacity: 0.8; overflow: hidden; text-overflow: ellipsis; @@ -403,7 +438,7 @@ class HuiSensorCard extends LitElement implements LovelaceCard { .info { flex-wrap: wrap; - margin: 16px 0 16px 8px; + margin: 16px; } #value { @@ -430,11 +465,17 @@ class HuiSensorCard extends LitElement implements LovelaceCard { margin-bottom: 0px; position: relative; width: 100%; + overflow: hidden; } .graph > div { align-self: flex-end; - margin: auto 8px; + margin: auto 0px; + display: flex; + } + + .fill { + opacity: 0.1; } `; } diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts index cd83334f83..ae5c5dff95 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -41,6 +41,7 @@ import { EntityRegistryEntry, } from "../../../data/entity_registry"; import { processEditorEntities } from "../editor/process-editor-entities"; +import { SENSOR_DEVICE_CLASS_BATTERY } from "../../../data/sensor"; const DEFAULT_VIEW_ENTITY_ID = "group.default_view"; const DOMAINS_BADGES = [ @@ -52,6 +53,7 @@ const DOMAINS_BADGES = [ "timer", ]; const HIDE_DOMAIN = new Set([ + "automation", "configurator", "device_tracker", "geo_location", @@ -180,6 +182,11 @@ export const computeCards = ( conf.icon = stateObj.attributes.icon; } entities.push(conf); + } else if ( + domain === "sensor" && + stateObj?.attributes.device_class === SENSOR_DEVICE_CLASS_BATTERY + ) { + // Do nothing. } else { let name: string; const entityConf = diff --git a/src/panels/lovelace/components/hui-card-options.ts b/src/panels/lovelace/components/hui-card-options.ts index fe360b2987..1223354fdd 100644 --- a/src/panels/lovelace/components/hui-card-options.ts +++ b/src/panels/lovelace/components/hui-card-options.ts @@ -33,72 +33,79 @@ export class HuiCardOptions extends LitElement { protected render(): TemplateResult { return html` -
-
- ${this.hass!.localize( - "ui.panel.lovelace.editor.edit_card.edit" - )} -
-
- - - + +
+
+ ${this.hass!.localize( + "ui.panel.lovelace.editor.edit_card.edit" + )} +
+
- - - ${this.hass!.localize( - "ui.panel.lovelace.editor.edit_card.move" - )} - - ${this.hass!.localize( - "ui.panel.lovelace.editor.edit_card.delete" - )} - - + + + + + + ${this.hass!.localize( + "ui.panel.lovelace.editor.edit_card.move" + )} + + ${this.hass!.localize( + "ui.panel.lovelace.editor.edit_card.delete" + )} + + +
-
+ `; } static get styles(): CSSResult { return css` - div.options { - border-top: 1px solid #e8e8e8; - padding: 5px 8px; - background: var(--paper-card-background-color, white); + ha-card { + border-top-right-radius: 0; + border-top-left-radius: 0; box-shadow: rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, rgba(0, 0, 0, 0.12) 0px 1px 5px -4px, rgba(0, 0, 0, 0.2) 0px 3px 1px -2px; + } + + div.options { + border-top: 1px solid #e8e8e8; + padding: 5px 8px; display: flex; + margin-top: -1px; } div.options .primary-actions { diff --git a/src/panels/lovelace/components/hui-timestamp-display.ts b/src/panels/lovelace/components/hui-timestamp-display.ts index f87fe820e8..809e559b39 100644 --- a/src/panels/lovelace/components/hui-timestamp-display.ts +++ b/src/panels/lovelace/components/hui-timestamp-display.ts @@ -8,15 +8,15 @@ import { } from "lit-element"; import { HomeAssistant } from "../../../types"; -import format_date from "../../../common/datetime/format_date"; -import format_date_time from "../../../common/datetime/format_date_time"; -import format_time from "../../../common/datetime/format_time"; +import { formatDate } from "../../../common/datetime/format_date"; +import { formatDateTime } from "../../../common/datetime/format_date_time"; +import { formatTime } from "../../../common/datetime/format_time"; import relativeTime from "../../../common/datetime/relative_time"; const FORMATS: { [key: string]: (ts: Date, lang: string) => string } = { - date: format_date, - datetime: format_date_time, - time: format_time, + date: formatDate, + datetime: formatDateTime, + time: formatTime, }; const INTERVAL_FORMAT = ["relative", "total"]; diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts index a81d54ceb3..bc19be2282 100755 --- a/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts @@ -84,7 +84,9 @@ export class HuiDialogSuggestCard extends LitElement { ${this._yamlMode && this._cardConfig ? html`
- +
` : ""} diff --git a/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts index 94d8d75994..00b596dff6 100644 --- a/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts @@ -11,9 +11,9 @@ import { EntitiesEditorEvent, EditorTarget } from "../types"; import { HomeAssistant } from "../../../../types"; import { LovelaceCardEditor } from "../../types"; import { fireEvent } from "../../../../common/dom/fire_event"; -import { MediaControlCardConfig } from "../../cards/hui-media-control-card"; import "../../../../components/entity/ha-entity-picker"; +import { MediaControlCardConfig } from "../../cards/types"; const cardConfigStruct = struct({ type: "string", diff --git a/src/panels/mailbox/ha-panel-mailbox.js b/src/panels/mailbox/ha-panel-mailbox.js index c2710282e0..f1f3158d53 100644 --- a/src/panels/mailbox/ha-panel-mailbox.js +++ b/src/panels/mailbox/ha-panel-mailbox.js @@ -14,7 +14,7 @@ import "../../components/ha-menu-button"; import "../../components/ha-card"; import "../../resources/ha-style"; -import formatDateTime from "../../common/datetime/format_date_time"; +import { formatDateTime } from "../../common/datetime/format_date_time"; import LocalizeMixin from "../../mixins/localize-mixin"; import { EventsMixin } from "../../mixins/events-mixin"; diff --git a/src/panels/map/ha-panel-map.js b/src/panels/map/ha-panel-map.js index 3fbf540354..1c083e4199 100644 --- a/src/panels/map/ha-panel-map.js +++ b/src/panels/map/ha-panel-map.js @@ -35,7 +35,7 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
[[localize('panel.map')]]
-