Merge pull request #7422 from home-assistant/dev

20201021.0
This commit is contained in:
Bram Kragten 2020-10-21 19:21:45 +02:00 committed by GitHub
commit 73be0fef75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
322 changed files with 15062 additions and 4070 deletions

View File

@ -75,13 +75,16 @@
"object-curly-newline": 0, "object-curly-newline": 0,
"default-case": 0, "default-case": 0,
"wc/no-self-class": 0, "wc/no-self-class": 0,
"no-shadow": 0,
"@typescript-eslint/camelcase": 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-use-before-define": 0,
"@typescript-eslint/no-non-null-assertion": 0, "@typescript-eslint/no-non-null-assertion": 0,
"@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/no-unused-vars": 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,
"@typescript-eslint/no-shadow": ["error"]
}, },
"plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"], "plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"],
"processor": "disable/disable" "processor": "disable/disable"

View File

@ -1,26 +0,0 @@
---
name: Request a feature for the UI, Frontend or Lovelace
about: Request an new feature for the Home Assistant frontend.
labels: feature request
---
<!--
DO NOT DELETE ANY TEXT from this template!
Otherwise, your request may be closed without comment.
-->
## The request
<!--
Describe to our maintainers, the feature you would like to be added.
Please be clear and concise and, if possible, provide a screenshot or mockup.
-->
## The alternatives
<!--
Are you currently using, or have you considered alternatives?
If so, could you please describe those?
-->
## Additional information

View File

@ -1,5 +1,8 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Request a feature for the UI, Frontend or Lovelace
url: https://github.com/home-assistant/frontend/discussions/category_choices
about: Request an new feature for the Home Assistant frontend.
- name: Report a bug that is NOT related to the UI, Frontend or Lovelace - name: Report a bug that is NOT related to the UI, Frontend or Lovelace
url: https://github.com/home-assistant/core/issues url: https://github.com/home-assistant/core/issues
about: This is the issue tracker for our frontend. Please report other issues with the backend repository. about: This is the issue tracker for our frontend. Please report other issues with the backend repository.

View File

@ -26,4 +26,4 @@ A complete guide can be found at the following [link](https://www.home-assistant
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects. Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.
We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variation of devices. We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variety of devices.

View File

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

View File

@ -2,7 +2,6 @@ const webpack = require("webpack");
const path = require("path"); const path = require("path");
const TerserPlugin = require("terser-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin");
const ManifestPlugin = require("webpack-manifest-plugin"); const ManifestPlugin = require("webpack-manifest-plugin");
const WorkerPlugin = require("worker-plugin");
const paths = require("./paths.js"); const paths = require("./paths.js");
const bundle = require("./bundle"); const bundle = require("./bundle");
@ -30,7 +29,7 @@ const createWebpackConfig = ({
module: { module: {
rules: [ rules: [
{ {
test: /\.js$|\.ts$/, test: /\.m?js$|\.ts$/,
exclude: bundle.babelExclude(), exclude: bundle.babelExclude(),
use: { use: {
loader: "babel-loader", loader: "babel-loader",
@ -54,8 +53,10 @@ const createWebpackConfig = ({
}), }),
], ],
}, },
experiments: {
topLevelAwait: true,
},
plugins: [ plugins: [
new WorkerPlugin(),
new ManifestPlugin({ new ManifestPlugin({
// Only include the JS of entrypoints // Only include the JS of entrypoints
filter: (file) => file.isInitial && !file.name.endsWith(".map"), filter: (file) => file.isInitial && !file.name.endsWith(".map"),
@ -110,6 +111,22 @@ const createWebpackConfig = ({
} }
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`; return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
}, },
environment: {
// The environment supports arrow functions ('() => { ... }').
arrowFunction: latestBuild,
// The environment supports BigInt as literal (123n).
bigIntLiteral: false,
// The environment supports const and let for variable declarations.
const: latestBuild,
// The environment supports destructuring ('{ a, b } = obj').
destructuring: latestBuild,
// The environment supports an async import() function to import EcmaScript modules.
dynamicImport: latestBuild,
// The environment supports 'for of' iteration ('for (const x of array) { ... }').
forOf: latestBuild,
// The environment supports ECMAScript Module syntax to import ECMAScript modules (import ... from '...').
module: latestBuild,
},
chunkFilename: chunkFilename:
isProdBuild && !isStatsBuild isProdBuild && !isStatsBuild
? "chunk.[chunkhash].js" ? "chunk.[chunkhash].js"

View File

@ -30,7 +30,7 @@ class HcLayout extends LitElement {
<ha-card> <ha-card>
<div class="layout"> <div class="layout">
<img class="hero" src="/images/google-nest-hub.png" /> <img class="hero" src="/images/google-nest-hub.png" />
<div class="card-header"> <h1 class="card-header">
Home Assistant Cast${this.subtitle ? ` ${this.subtitle}` : ""} Home Assistant Cast${this.subtitle ? ` ${this.subtitle}` : ""}
${this.auth ${this.auth
? html` ? html`
@ -44,7 +44,7 @@ class HcLayout extends LitElement {
</div> </div>
` `
: ""} : ""}
</div> </h1>
<slot></slot> <slot></slot>
</div> </div>
</ha-card> </ha-card>

View File

@ -23,9 +23,9 @@ import { hassioStyle } from "../resources/hassio-style";
class HassioAddonRepositoryEl extends LitElement { class HassioAddonRepositoryEl extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public repo!: HassioAddonRepository; @property({ attribute: false }) public repo!: HassioAddonRepository;
@property() public addons!: HassioAddonInfo[]; @property({ attribute: false }) public addons!: HassioAddonInfo[];
@property() public filter!: string; @property() public filter!: string;
@ -78,18 +78,18 @@ class HassioAddonRepositoryEl extends LitElement {
.title=${addon.name} .title=${addon.name}
.description=${addon.description} .description=${addon.description}
.available=${addon.available} .available=${addon.available}
.icon=${addon.installed && addon.installed !== addon.version .icon=${addon.installed && addon.update_available
? mdiArrowUpBoldCircle ? mdiArrowUpBoldCircle
: mdiPuzzle} : mdiPuzzle}
.iconTitle=${addon.installed .iconTitle=${addon.installed
? addon.installed !== addon.version ? addon.update_available
? "New version available" ? "New version available"
: "Add-on is installed" : "Add-on is installed"
: addon.available : addon.available
? "Add-on is not installed" ? "Add-on is not installed"
: "Add-on is not available on your system"} : "Add-on is not available on your system"}
.iconClass=${addon.installed .iconClass=${addon.installed
? addon.installed !== addon.version ? addon.update_available
? "update" ? "update"
: "installed" : "installed"
: !addon.available : !addon.available
@ -104,7 +104,7 @@ class HassioAddonRepositoryEl extends LitElement {
: undefined} : undefined}
.showTopbar=${addon.installed || !addon.available} .showTopbar=${addon.installed || !addon.available}
.topbarClass=${addon.installed .topbarClass=${addon.installed
? addon.installed !== addon.version ? addon.update_available
? "update" ? "update"
: "installed" : "installed"
: !addon.available : !addon.available

View File

@ -11,6 +11,7 @@ import {
PropertyValues, PropertyValues,
} from "lit-element"; } from "lit-element";
import { html, TemplateResult } from "lit-html"; import { html, TemplateResult } from "lit-html";
import { atLeastVersion } from "../../../src/common/config/version";
import "../../../src/common/search/search-input"; import "../../../src/common/search/search-input";
import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-svg-icon"; import "../../../src/components/ha-svg-icon";
@ -24,6 +25,7 @@ import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import "../../../src/layouts/hass-loading-screen"; import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-tabs-subpage"; import "../../../src/layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../../src/types"; import { HomeAssistant, Route } from "../../../src/types";
import { showRegistriesDialog } from "../dialogs/registries/show-dialog-registries";
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories"; import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
import { supervisorTabs } from "../hassio-tabs"; import { supervisorTabs } from "../hassio-tabs";
import "./hassio-addon-repository"; import "./hassio-addon-repository";
@ -98,14 +100,14 @@ class HassioAddonStore extends LitElement {
main-page main-page
.tabs=${supervisorTabs} .tabs=${supervisorTabs}
> >
<span slot="header">Add-on store</span> <span slot="header">Add-on Store</span>
<ha-button-menu <ha-button-menu
corner="BOTTOM_START" corner="BOTTOM_START"
slot="toolbar-icon" slot="toolbar-icon"
@action=${this._handleAction} @action=${this._handleAction}
> >
<mwc-icon-button slot="trigger" alt="menu"> <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-icon-button>
<mwc-list-item> <mwc-list-item>
Repositories Repositories
@ -113,6 +115,12 @@ class HassioAddonStore extends LitElement {
<mwc-list-item> <mwc-list-item>
Reload Reload
</mwc-list-item> </mwc-list-item>
${this.hass.userData?.showAdvanced &&
atLeastVersion(this.hass.config.version, 0, 117)
? html`<mwc-list-item>
Registries
</mwc-list-item>`
: ""}
</ha-button-menu> </ha-button-menu>
${repos.length === 0 ${repos.length === 0
? html`<hass-loading-screen no-toolbar></hass-loading-screen>` ? html`<hass-loading-screen no-toolbar></hass-loading-screen>`
@ -157,6 +165,9 @@ class HassioAddonStore extends LitElement {
case 1: case 1:
this.refreshData(); this.refreshData();
break; break;
case 2:
this._manageRegistries();
break;
} }
} }
@ -173,6 +184,10 @@ class HassioAddonStore extends LitElement {
}); });
} }
private async _manageRegistries() {
showRegistriesDialog(this);
}
private async _loadData() { private async _loadData() {
try { try {
const addonsInfo = await fetchHassioAddonsInfo(this.hass); const addonsInfo = await fetchHassioAddonsInfo(this.hass);

View File

@ -39,13 +39,11 @@ class HassioAddonConfig extends LitElement {
@property({ type: Boolean }) private _configHasChanged = false; @property({ type: Boolean }) private _configHasChanged = false;
@query("ha-yaml-editor") private _editor!: HaYamlEditor; @property({ type: Boolean }) private _valid = true;
@query("ha-yaml-editor", true) private _editor!: HaYamlEditor;
protected render(): TemplateResult { protected render(): TemplateResult {
const editor = this._editor;
// If editor not rendered, don't show the error.
const valid = editor ? editor.isValid : true;
return html` return html`
<h1>${this.addon.name}</h1> <h1>${this.addon.name}</h1>
<ha-card header="Configuration"> <ha-card header="Configuration">
@ -54,7 +52,7 @@ class HassioAddonConfig extends LitElement {
@value-changed=${this._configChanged} @value-changed=${this._configChanged}
></ha-yaml-editor> ></ha-yaml-editor>
${this._error ? html` <div class="errors">${this._error}</div> ` : ""} ${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
${valid ? "" : html` <div class="errors">Invalid YAML</div> `} ${this._valid ? "" : html` <div class="errors">Invalid YAML</div> `}
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-progress-button class="warning" @click=${this._resetTapped}> <ha-progress-button class="warning" @click=${this._resetTapped}>
@ -62,7 +60,7 @@ class HassioAddonConfig extends LitElement {
</ha-progress-button> </ha-progress-button>
<ha-progress-button <ha-progress-button
@click=${this._saveTapped} @click=${this._saveTapped}
.disabled=${!this._configHasChanged || !valid} .disabled=${!this._configHasChanged || !this._valid}
> >
Save Save
</ha-progress-button> </ha-progress-button>
@ -78,9 +76,9 @@ class HassioAddonConfig extends LitElement {
} }
} }
private _configChanged(): void { private _configChanged(ev): void {
this._configHasChanged = true; this._configHasChanged = true;
this.requestUpdate(); this._valid = ev.detail.isValid;
} }
private async _resetTapped(ev: CustomEvent): Promise<void> { private async _resetTapped(ev: CustomEvent): Promise<void> {

View File

@ -69,7 +69,7 @@ const STAGE_ICON = {
const PERMIS_DESC = { const PERMIS_DESC = {
stage: { stage: {
title: "Add-on 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: { rating: {
title: "Add-on Security Rating", title: "Add-on Security Rating",
@ -135,7 +135,7 @@ class HassioAddonInfo extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
${this._computeUpdateAvailable ${this.addon.update_available
? html` ? html`
<ha-card header="Update available! 🎉"> <ha-card header="Update available! 🎉">
<div class="card-content"> <div class="card-content">
@ -178,7 +178,7 @@ class HassioAddonInfo extends LitElement {
${!this.addon.protected ${!this.addon.protected
? html` ? html`
<ha-card class="warning"> <ha-card class="warning">
<div class="card-header">Warning: Protection mode is disabled!</div> <h1 class="card-header">Warning: Protection mode is disabled!</h1>
<div class="card-content"> <div class="card-content">
Protection mode on this add-on is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this add-on. Protection mode on this add-on is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this add-on.
</div> </div>
@ -202,14 +202,14 @@ class HassioAddonInfo extends LitElement {
<ha-svg-icon <ha-svg-icon
title="Add-on is running" title="Add-on is running"
class="running" class="running"
path=${mdiCircle} .path=${mdiCircle}
></ha-svg-icon> ></ha-svg-icon>
` `
: html` : html`
<ha-svg-icon <ha-svg-icon
title="Add-on is stopped" title="Add-on is stopped"
class="stopped" class="stopped"
path=${mdiCircle} .path=${mdiCircle}
></ha-svg-icon> ></ha-svg-icon>
`} `}
` `
@ -283,7 +283,7 @@ class HassioAddonInfo extends LitElement {
label="host" label="host"
description="" description=""
> >
<ha-svg-icon path=${mdiNetwork}></ha-svg-icon> <ha-svg-icon .path=${mdiNetwork}></ha-svg-icon>
</ha-label-badge> </ha-label-badge>
` `
: ""} : ""}
@ -295,7 +295,7 @@ class HassioAddonInfo extends LitElement {
label="hardware" label="hardware"
description="" description=""
> >
<ha-svg-icon path=${mdiChip}></ha-svg-icon> <ha-svg-icon .path=${mdiChip}></ha-svg-icon>
</ha-label-badge> </ha-label-badge>
` `
: ""} : ""}
@ -307,7 +307,7 @@ class HassioAddonInfo extends LitElement {
label="hass" label="hass"
description="" description=""
> >
<ha-svg-icon path=${mdiHomeAssistant}></ha-svg-icon> <ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</ha-label-badge> </ha-label-badge>
` `
: ""} : ""}
@ -319,7 +319,7 @@ class HassioAddonInfo extends LitElement {
label="hassio" label="hassio"
.description=${this.addon.hassio_role} .description=${this.addon.hassio_role}
> >
<ha-svg-icon path=${mdiHomeAssistant}></ha-svg-icon> <ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</ha-label-badge> </ha-label-badge>
` `
: ""} : ""}
@ -331,7 +331,7 @@ class HassioAddonInfo extends LitElement {
label="docker" label="docker"
description="" description=""
> >
<ha-svg-icon path=${mdiDocker}></ha-svg-icon> <ha-svg-icon .path=${mdiDocker}></ha-svg-icon>
</ha-label-badge> </ha-label-badge>
` `
: ""} : ""}
@ -343,7 +343,7 @@ class HassioAddonInfo extends LitElement {
label="host pid" label="host pid"
description="" description=""
> >
<ha-svg-icon path=${mdiPound}></ha-svg-icon> <ha-svg-icon .path=${mdiPound}></ha-svg-icon>
</ha-label-badge> </ha-label-badge>
` `
: ""} : ""}
@ -356,7 +356,7 @@ class HassioAddonInfo extends LitElement {
label="apparmor" label="apparmor"
description="" description=""
> >
<ha-svg-icon path=${mdiShield}></ha-svg-icon> <ha-svg-icon .path=${mdiShield}></ha-svg-icon>
</ha-label-badge> </ha-label-badge>
` `
: ""} : ""}
@ -368,7 +368,7 @@ class HassioAddonInfo extends LitElement {
label="auth" label="auth"
description="" description=""
> >
<ha-svg-icon path=${mdiKey}></ha-svg-icon> <ha-svg-icon .path=${mdiKey}></ha-svg-icon>
</ha-label-badge> </ha-label-badge>
` `
: ""} : ""}
@ -381,7 +381,7 @@ class HassioAddonInfo extends LitElement {
description="" description=""
> >
<ha-svg-icon <ha-svg-icon
path=${mdiCursorDefaultClickOutline} .path=${mdiCursorDefaultClickOutline}
></ha-svg-icon> ></ha-svg-icon>
</ha-label-badge> </ha-label-badge>
` `
@ -609,15 +609,6 @@ class HassioAddonInfo extends LitElement {
return this.addon?.state === "started"; return this.addon?.state === "started";
} }
private get _computeUpdateAvailable(): boolean | "" {
return (
this.addon &&
!this.addon.detached &&
this.addon.version &&
this.addon.version !== this.addon.version_latest
);
}
private get _pathWebui(): string | null { private get _pathWebui(): string | null {
return ( return (
this.addon.webui && this.addon.webui &&
@ -798,10 +789,10 @@ class HassioAddonInfo extends LitElement {
); );
if (!validate.data.valid) { if (!validate.data.valid) {
await showConfirmationDialog(this, { 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], text: validate.data.message.split(" Got ")[0],
confirm: () => this._openConfiguration(), confirm: () => this._openConfiguration(),
confirmText: "Go to configruation", confirmText: "Go to configuration",
dismissText: "Cancel", dismissText: "Cancel",
}); });
button.progress = false; button.progress = false;

View File

@ -50,7 +50,7 @@ class HassioCardContent extends LitElement {
` `
: html` : html`
<ha-svg-icon <ha-svg-icon
class=${this.iconClass} class=${this.iconClass!}
.path=${this.icon} .path=${this.icon}
.title=${this.iconTitle} .title=${this.iconTitle}
></ha-svg-icon> ></ha-svg-icon>

View File

@ -1,4 +1,3 @@
import "../../../src/components/ha-file-upload";
import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-icon-button/mwc-icon-button";
import { mdiFolderUpload } from "@mdi/js"; import { mdiFolderUpload } from "@mdi/js";
import "@polymer/iron-input/iron-input"; import "@polymer/iron-input/iron-input";
@ -12,13 +11,15 @@ import {
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../../../src/common/dom/fire_event"; import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-circular-progress"; import "../../../src/components/ha-circular-progress";
import "../../../src/components/ha-file-upload";
import "../../../src/components/ha-svg-icon"; import "../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { import {
HassioSnapshot, HassioSnapshot,
uploadSnapshot, uploadSnapshot,
} from "../../../src/data/hassio/snapshot"; } from "../../../src/data/hassio/snapshot";
import { HomeAssistant } from "../../../src/types";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import { HomeAssistant } from "../../../src/types";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
@ -65,7 +66,7 @@ export class HassioUploadSnapshot extends LitElement {
} catch (err) { } catch (err) {
showAlertDialog(this, { showAlertDialog(this, {
title: "Upload failed", title: "Upload failed",
text: err.toString(), text: extractApiErrorMessage(err),
confirmText: "ok", confirmText: "ok",
}); });
} finally { } finally {

View File

@ -52,22 +52,21 @@ class HassioAddons extends LitElement {
.title=${addon.name} .title=${addon.name}
.description=${addon.description} .description=${addon.description}
available available
.showTopbar=${addon.installed !== addon.version} .showTopbar=${addon.update_available}
topbarClass="update" topbarClass="update"
.icon=${addon.installed !== addon.version .icon=${addon.update_available!
? mdiArrowUpBoldCircle ? mdiArrowUpBoldCircle
: mdiPuzzle} : mdiPuzzle}
.iconTitle=${addon.state !== "started" .iconTitle=${addon.state !== "started"
? "Add-on is stopped" ? "Add-on is stopped"
: addon.installed !== addon.version : addon.update_available!
? "New version available" ? "New version available"
: "Add-on is running"} : "Add-on is running"}
.iconClass=${addon.installed && .iconClass=${addon.update_available
addon.installed !== addon.version
? addon.state === "started" ? addon.state === "started"
? "update" ? "update"
: "update stopped" : "update stopped"
: addon.installed && addon.state === "started" : addon.state === "started"
? "running" ? "running"
: "stopped"} : "stopped"}
.iconImage=${atLeastVersion( .iconImage=${atLeastVersion(

View File

@ -5,11 +5,11 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import memoizeOne from "memoize-one";
import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-svg-icon"; import "../../../src/components/ha-svg-icon";
@ -35,29 +35,30 @@ import { hassioStyle } from "../resources/hassio-style";
export class HassioUpdate extends LitElement { export class HassioUpdate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public hassInfo: HassioHomeAssistantInfo; @property({ attribute: false }) public hassInfo?: HassioHomeAssistantInfo;
@property({ attribute: false }) public hassOsInfo?: HassioHassOSInfo; @property({ attribute: false }) public hassOsInfo?: HassioHassOSInfo;
@property() public supervisorInfo: HassioSupervisorInfo; @property({ attribute: false }) public supervisorInfo?: HassioSupervisorInfo;
@internalProperty() private _error?: string; private _pendingUpdates = memoizeOne(
(
core?: HassioHomeAssistantInfo,
supervisor?: HassioSupervisorInfo,
os?: HassioHassOSInfo
): number => {
return [core, supervisor, os].filter(
(value) => !!value && value?.update_available
).length;
}
);
protected render(): TemplateResult { protected render(): TemplateResult {
const updatesAvailable: number = [ const updatesAvailable = this._pendingUpdates(
this.hassInfo, this.hassInfo,
this.supervisorInfo, this.supervisorInfo,
this.hassOsInfo, this.hassOsInfo
].filter((value) => {
return (
!!value &&
(value.version_latest
? value.version !== value.version_latest
: value.version_latest
? value.version !== value.version_latest
: false)
); );
}).length;
if (!updatesAvailable) { if (!updatesAvailable) {
return html``; return html``;
@ -65,9 +66,6 @@ export class HassioUpdate extends LitElement {
return html` return html`
<div class="content"> <div class="content">
${this._error
? html` <div class="error">Error: ${this._error}</div> `
: ""}
<h1> <h1>
${updatesAvailable > 1 ${updatesAvailable > 1
? "Updates Available 🎉" ? "Updates Available 🎉"
@ -76,26 +74,24 @@ export class HassioUpdate extends LitElement {
<div class="card-group"> <div class="card-group">
${this._renderUpdateCard( ${this._renderUpdateCard(
"Home Assistant Core", "Home Assistant Core",
this.hassInfo.version, this.hassInfo!,
this.hassInfo.version_latest,
"hassio/homeassistant/update", "hassio/homeassistant/update",
`https://${ `https://${
this.hassInfo.version_latest.includes("b") ? "rc" : "www" this.hassInfo?.version_latest.includes("b") ? "rc" : "www"
}.home-assistant.io/latest-release-notes/`, }.home-assistant.io/latest-release-notes/`
mdiHomeAssistant
)} )}
${this._renderUpdateCard( ${this._renderUpdateCard(
"Supervisor", "Supervisor",
this.supervisorInfo.version, this.supervisorInfo!,
this.supervisorInfo.version_latest,
"hassio/supervisor/update", "hassio/supervisor/update",
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisorInfo.version_latest}` `https://github.com//home-assistant/hassio/releases/tag/${
this.supervisorInfo!.version_latest
}`
)} )}
${this.hassOsInfo ${this.hassOsInfo
? this._renderUpdateCard( ? this._renderUpdateCard(
"Operating System", "Operating System",
this.hassOsInfo.version, this.hassOsInfo,
this.hassOsInfo.version_latest,
"hassio/os/update", "hassio/os/update",
`https://github.com//home-assistant/hassos/releases/tag/${this.hassOsInfo.version_latest}` `https://github.com//home-assistant/hassos/releases/tag/${this.hassOsInfo.version_latest}`
) )
@ -107,28 +103,22 @@ export class HassioUpdate extends LitElement {
private _renderUpdateCard( private _renderUpdateCard(
name: string, name: string,
curVersion: string, object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo,
lastVersion: string,
apiPath: string, apiPath: string,
releaseNotesUrl: string, releaseNotesUrl: string
icon?: string
): TemplateResult { ): TemplateResult {
if (!lastVersion || lastVersion === curVersion) { if (!object.update_available) {
return html``; return html``;
} }
return html` return html`
<ha-card> <ha-card>
<div class="card-content"> <div class="card-content">
${icon
? html`
<div class="icon"> <div class="icon">
<ha-svg-icon .path=${icon}></ha-svg-icon> <ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</div> </div>
` <div class="update-heading">${name} ${object.version_latest}</div>
: ""}
<div class="update-heading">${name} ${lastVersion}</div>
<div class="warning"> <div class="warning">
You are currently running version ${curVersion} You are currently running version ${object.version}
</div> </div>
</div> </div>
<div class="card-actions"> <div class="card-actions">
@ -138,7 +128,7 @@ export class HassioUpdate extends LitElement {
<ha-progress-button <ha-progress-button
.apiPath=${apiPath} .apiPath=${apiPath}
.name=${name} .name=${name}
.version=${lastVersion} .version=${object.version_latest}
@click=${this._confirmUpdate} @click=${this._confirmUpdate}
> >
Update Update

View File

@ -39,7 +39,8 @@ import type { HomeAssistant } from "../../../../src/types";
import { HassioNetworkDialogParams } from "./show-dialog-network"; import { HassioNetworkDialogParams } from "./show-dialog-network";
@customElement("dialog-hassio-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; @property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _prosessing = false; @internalProperty() private _prosessing = false;

View File

@ -0,0 +1,245 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button/mwc-icon-button";
import "@material/mwc-list/mwc-list-item";
import { mdiDelete } from "@mdi/js";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import "../../../../src/components/ha-circular-progress";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
addHassioDockerRegistry,
fetchHassioDockerRegistries,
removeHassioDockerRegistry,
} from "../../../../src/data/hassio/docker";
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
@customElement("dialog-hassio-registries")
class HassioRegistriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) private _registries?: {
registry: string;
username: string;
}[];
@internalProperty() private _registry?: string;
@internalProperty() private _username?: string;
@internalProperty() private _password?: string;
@internalProperty() private _opened = false;
@internalProperty() private _addingRegistry = false;
protected render(): TemplateResult {
return html`
<ha-dialog
.open=${this._opened}
@closing=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this._addingRegistry
? "Add New Docker Registry"
: "Manage Docker Registries"
)}
>
<div class="form">
${this._addingRegistry
? html`
<paper-input
@value-changed=${this._inputChanged}
class="flex-auto"
name="registry"
label="Registry"
required
auto-validate
></paper-input>
<paper-input
@value-changed=${this._inputChanged}
class="flex-auto"
name="username"
label="Username"
required
auto-validate
></paper-input>
<paper-input
@value-changed=${this._inputChanged}
class="flex-auto"
name="password"
label="Password"
type="password"
required
auto-validate
></paper-input>
<mwc-button
?disabled=${Boolean(
!this._registry || !this._username || !this._password
)}
@click=${this._addNewRegistry}
>
Add registry
</mwc-button>
`
: html`${this._registries?.length
? this._registries.map((entry) => {
return html`
<mwc-list-item class="option" hasMeta twoline>
<span>${entry.registry}</span>
<span slot="secondary"
>Username: ${entry.username}</span
>
<mwc-icon-button
.entry=${entry}
title="Remove"
slot="meta"
@click=${this._removeRegistry}
>
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
</mwc-icon-button>
</mwc-list-item>
`;
})
: html`
<mwc-list-item>
<span>No registries configured</span>
</mwc-list-item>
`}
<mwc-button @click=${this._addRegistry}>
Add new registry
</mwc-button> `}
</div>
</ha-dialog>
`;
}
private _inputChanged(ev: Event) {
const target = ev.currentTarget as PaperInputElement;
this[`_${target.name}`] = target.value;
}
public async showDialog(_dialogParams: any): Promise<void> {
this._opened = true;
await this._loadRegistries();
await this.updateComplete;
}
public closeDialog(): void {
this._addingRegistry = false;
this._opened = false;
}
public focus(): void {
this.updateComplete.then(() =>
(this.shadowRoot?.querySelector(
"[dialogInitialFocus]"
) as HTMLElement)?.focus()
);
}
private async _loadRegistries(): Promise<void> {
const registries = await fetchHassioDockerRegistries(this.hass);
this._registries = Object.keys(registries!.registries).map((key) => ({
registry: key,
username: registries.registries[key].username,
}));
}
private _addRegistry(): void {
this._addingRegistry = true;
}
private async _addNewRegistry(): Promise<void> {
const data = {};
data[this._registry!] = {
username: this._username,
password: this._password,
};
try {
await addHassioDockerRegistry(this.hass, data);
await this._loadRegistries();
this._addingRegistry = false;
} catch (err) {
showAlertDialog(this, {
title: "Failed to add registry",
text: extractApiErrorMessage(err),
});
}
}
private async _removeRegistry(ev: Event): Promise<void> {
const entry = (ev.currentTarget as any).entry;
try {
await removeHassioDockerRegistry(this.hass, entry.registry);
await this._loadRegistries();
} catch (err) {
showAlertDialog(this, {
title: "Failed to remove registry",
text: extractApiErrorMessage(err),
});
}
}
static get styles(): CSSResult[] {
return [
haStyle,
haStyleDialog,
css`
ha-dialog.button-left {
--justify-action-buttons: flex-start;
}
paper-icon-item {
cursor: pointer;
}
.form {
color: var(--primary-text-color);
}
.option {
border: 1px solid var(--divider-color);
border-radius: 4px;
margin-top: 4px;
}
mwc-button {
margin-left: 8px;
}
mwc-icon-button {
color: var(--error-color);
margin: -10px;
}
mwc-list-item {
cursor: default;
}
mwc-list-item span[slot="secondary"] {
color: var(--secondary-text-color);
}
ha-paper-dropdown-menu {
display: block;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-registries": HassioRegistriesDialog;
}
}

View File

@ -0,0 +1,13 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "./dialog-hassio-registries";
export const showRegistriesDialog = (element: HTMLElement): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-registries",
dialogImport: () =>
import(
/* webpackChunkName: "dialog-hassio-registries" */ "./dialog-hassio-registries"
),
dialogParams: {},
});
};

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { mdiClose, mdiDelete, mdiDownload, mdiHistory } from "@mdi/js"; import { mdiClose, mdiDelete, mdiDownload, mdiHistory } from "@mdi/js";
import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox"; import "@polymer/paper-checkbox/paper-checkbox";
import type { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { import {
css, css,
@ -196,7 +197,7 @@ class HassioSnapshotDialog extends LitElement {
@click=${this._downloadClicked} @click=${this._downloadClicked}
slot="primaryAction" slot="primaryAction"
> >
<ha-svg-icon path=${mdiDownload} class="icon"></ha-svg-icon> <ha-svg-icon .path=${mdiDownload} class="icon"></ha-svg-icon>
Download Snapshot Download Snapshot
</mwc-button>` </mwc-button>`
: ""} : ""}
@ -205,7 +206,7 @@ class HassioSnapshotDialog extends LitElement {
@click=${this._partialRestoreClicked} @click=${this._partialRestoreClicked}
slot="secondaryAction" slot="secondaryAction"
> >
<ha-svg-icon path=${mdiHistory} class="icon"></ha-svg-icon> <ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon>
Restore Selected Restore Selected
</mwc-button> </mwc-button>
${this._snapshot.type === "full" ${this._snapshot.type === "full"
@ -214,7 +215,7 @@ class HassioSnapshotDialog extends LitElement {
@click=${this._fullRestoreClicked} @click=${this._fullRestoreClicked}
slot="secondaryAction" 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 Wipe &amp; restore
</mwc-button> </mwc-button>
` `
@ -224,7 +225,10 @@ class HassioSnapshotDialog extends LitElement {
@click=${this._deleteClicked} @click=${this._deleteClicked}
slot="secondaryAction" 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> <span class="warning">Delete Snapshot</span>
</mwc-button>` </mwc-button>`
: ""} : ""}
@ -440,6 +444,19 @@ class HassioSnapshotDialog extends LitElement {
return; 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 name = this._computeName.replace(/[^a-z0-9]+/gi, "_");
const a = document.createElement("a"); const a = document.createElement("a");
a.href = signedPath.path; a.href = signedPath.path;

View File

@ -25,13 +25,13 @@ class HassioPanelRouter extends HassRouterPage {
@property({ type: Boolean }) public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public supervisorInfo: HassioSupervisorInfo; @property({ attribute: false }) public supervisorInfo?: HassioSupervisorInfo;
@property({ attribute: false }) public hassioInfo!: HassioInfo; @property({ attribute: false }) public hassioInfo!: HassioInfo;
@property({ attribute: false }) public hostInfo: HassioHostInfo; @property({ attribute: false }) public hostInfo?: HassioHostInfo;
@property({ attribute: false }) public hassInfo: HassioHomeAssistantInfo; @property({ attribute: false }) public hassInfo?: HassioHomeAssistantInfo;
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo; @property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;

View File

@ -66,15 +66,15 @@ class HassioRouter extends HassRouterPage {
}, },
}; };
@internalProperty() private _supervisorInfo: HassioSupervisorInfo; @internalProperty() private _supervisorInfo?: HassioSupervisorInfo;
@internalProperty() private _hostInfo: HassioHostInfo; @internalProperty() private _hostInfo?: HassioHostInfo;
@internalProperty() private _hassioInfo?: HassioInfo; @internalProperty() private _hassioInfo?: HassioInfo;
@internalProperty() private _hassOsInfo?: HassioHassOSInfo; @internalProperty() private _hassOsInfo?: HassioHassOSInfo;
@internalProperty() private _hassInfo: HassioHomeAssistantInfo; @internalProperty() private _hassInfo?: HassioHomeAssistantInfo;
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);

View File

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

View File

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

View File

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

View File

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

View File

@ -7,18 +7,21 @@ import {
property, property,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row"; import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-switch"; import "../../../src/components/ha-switch";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { HassioHostInfo as HassioHostInfoType } from "../../../src/data/hassio/host"; import { HassioHostInfo as HassioHostInfoType } from "../../../src/data/hassio/host";
import { fetchHassioResolution } from "../../../src/data/hassio/resolution";
import { import {
fetchHassioSupervisorInfo,
HassioSupervisorInfo as HassioSupervisorInfoType, HassioSupervisorInfo as HassioSupervisorInfoType,
reloadSupervisor, reloadSupervisor,
setSupervisorOption, setSupervisorOption,
SupervisorOptions, SupervisorOptions,
updateSupervisor, updateSupervisor,
fetchHassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor"; } from "../../../src/data/hassio/supervisor";
import { import {
showAlertDialog, showAlertDialog,
@ -26,14 +29,42 @@ import {
} from "../../../src/dialogs/generic/show-dialog-box"; } from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import { documentationUrl } from "../../../src/util/documentation-url";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
const ISSUES = {
container: {
title: "Containers known to cause issues",
url: "/more-info/unsupported/container",
},
dbus: { title: "DBUS", url: "/more-info/unsupported/dbus" },
docker_configuration: {
title: "Docker Configuration",
url: "/more-info/unsupported/docker_configuration",
},
docker_version: {
title: "Docker Version",
url: "/more-info/unsupported/docker_version",
},
lxc: { title: "LXC", url: "/more-info/unsupported/lxc" },
network_manager: {
title: "Network Manager",
url: "/more-info/unsupported/network_manager",
},
os: { title: "Operating System", url: "/more-info/unsupported/os" },
privileged: {
title: "Supervisor is not privileged",
url: "/more-info/unsupported/privileged",
},
systemd: { title: "Systemd", url: "/more-info/unsupported/systemd" },
};
@customElement("hassio-supervisor-info") @customElement("hassio-supervisor-info")
class HassioSupervisorInfo extends LitElement { class HassioSupervisorInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public supervisorInfo!: HassioSupervisorInfoType; @property({ attribute: false })
public supervisorInfo!: HassioSupervisorInfoType;
@property() public hostInfo!: HassioHostInfoType; @property() public hostInfo!: HassioHostInfoType;
@ -51,12 +82,12 @@ class HassioSupervisorInfo extends LitElement {
</ha-settings-row> </ha-settings-row>
<ha-settings-row> <ha-settings-row>
<span slot="heading"> <span slot="heading">
Newest version Newest Version
</span> </span>
<span slot="description"> <span slot="description">
${this.supervisorInfo.version_latest} ${this.supervisorInfo.version_latest}
</span> </span>
${this.supervisorInfo.version !== this.supervisorInfo.version_latest ${this.supervisorInfo.update_available
? html` ? html`
<ha-progress-button <ha-progress-button
title="Update the supervisor" title="Update the supervisor"
@ -98,7 +129,7 @@ class HassioSupervisorInfo extends LitElement {
${this.supervisorInfo?.supported ${this.supervisorInfo?.supported
? html` <ha-settings-row three-line> ? html` <ha-settings-row three-line>
<span slot="heading"> <span slot="heading">
Share diagnostics Share Diagnostics
</span> </span>
<div slot="description" class="diagnostics-description"> <div slot="description" class="diagnostics-description">
Share crash reports and diagnostic information. Share crash reports and diagnostic information.
@ -118,24 +149,19 @@ class HassioSupervisorInfo extends LitElement {
</ha-settings-row>` </ha-settings-row>`
: html`<div class="error"> : html`<div class="error">
You are running an unsupported installation. You are running an unsupported installation.
<a <button
href="https://github.com/home-assistant/architecture/blob/master/adr/${this.hostInfo.features.includes( class="link"
"hassos"
)
? "0015-home-assistant-os.md"
: "0014-home-assistant-supervised.md"}"
target="_blank"
rel="noreferrer"
title="Learn more about how you can make your system compliant" title="Learn more about how you can make your system compliant"
@click=${this._unsupportedDialog}
> >
Learn More Learn more
</a> </button>
</div>`} </div>`}
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-progress-button <ha-progress-button
@click=${this._supervisorReload} @click=${this._supervisorReload}
title="Reload parts of the supervisor." title="Reload parts of the supervisor"
> >
Reload Reload
</ha-progress-button> </ha-progress-button>
@ -181,7 +207,7 @@ class HassioSupervisorInfo extends LitElement {
}; };
await setSupervisorOption(this.hass, data); await setSupervisorOption(this.hass, data);
await reloadSupervisor(this.hass); await reloadSupervisor(this.hass);
this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass); fireEvent(this, "hass-api-called", { success: true, response: null });
} catch (err) { } catch (err) {
showAlertDialog(this, { showAlertDialog(this, {
title: "Failed to set supervisor option", title: "Failed to set supervisor option",
@ -212,7 +238,7 @@ class HassioSupervisorInfo extends LitElement {
button.progress = true; button.progress = true;
const confirmed = await showConfirmationDialog(this, { 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}?`, text: `Are you sure you want to update supervisor to version ${this.supervisorInfo.version_latest}?`,
confirmText: "update", confirmText: "update",
dismissText: "cancel", dismissText: "cancel",
@ -249,6 +275,32 @@ class HassioSupervisorInfo extends LitElement {
}); });
} }
private async _unsupportedDialog(): Promise<void> {
const resolution = await fetchHassioResolution(this.hass);
await showAlertDialog(this, {
title: "You are running an unsupported installation",
text: html`Below is a list of issues found with your installation, click
on the links to learn how you can resolve the issues. <br /><br />
<ul>
${resolution.unsupported.map(
(issue) => html`
<li>
${ISSUES[issue]
? html`<a
href="${documentationUrl(this.hass, ISSUES[issue].url)}"
target="_blank"
rel="noreferrer"
>
${ISSUES[issue].title}
</a>`
: issue}
</li>
`
)}
</ul>`,
});
}
private async _toggleDiagnostics(): Promise<void> { private async _toggleDiagnostics(): Promise<void> {
try { try {
const data: SupervisorOptions = { const data: SupervisorOptions = {

View File

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

View File

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

View File

@ -22,28 +22,29 @@
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)", "author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@formatjs/intl-pluralrules": "^1.5.8", "@formatjs/intl-getcanonicallocales": "^1.4.6",
"@formatjs/intl-pluralrules": "^3.4.10",
"@fullcalendar/common": "5.1.0", "@fullcalendar/common": "5.1.0",
"@fullcalendar/core": "5.1.0", "@fullcalendar/core": "5.1.0",
"@fullcalendar/daygrid": "5.1.0", "@fullcalendar/daygrid": "5.1.0",
"@fullcalendar/interaction": "5.1.0", "@fullcalendar/interaction": "5.1.0",
"@fullcalendar/list": "5.1.0", "@fullcalendar/list": "5.1.0",
"@material/chips": "=8.0.0-canary.096a7a066.0", "@material/chips": "=8.0.0-canary.774dcfc8e.0",
"@material/circular-progress": "=8.0.0-canary.a78ceb112.0", "@material/mwc-button": "^0.19.0",
"@material/mwc-button": "^0.18.0", "@material/mwc-checkbox": "^0.19.0",
"@material/mwc-checkbox": "^0.18.0", "@material/mwc-circular-progress": "^0.19.0",
"@material/mwc-dialog": "^0.18.0", "@material/mwc-dialog": "^0.19.0",
"@material/mwc-fab": "^0.18.0", "@material/mwc-fab": "^0.19.0",
"@material/mwc-formfield": "^0.18.0", "@material/mwc-formfield": "^0.19.0",
"@material/mwc-icon-button": "^0.18.0", "@material/mwc-icon-button": "^0.19.0",
"@material/mwc-list": "^0.18.0", "@material/mwc-list": "^0.19.0",
"@material/mwc-menu": "^0.18.0", "@material/mwc-menu": "^0.19.0",
"@material/mwc-radio": "^0.18.0", "@material/mwc-radio": "^0.19.0",
"@material/mwc-ripple": "^0.18.0", "@material/mwc-ripple": "^0.19.0",
"@material/mwc-switch": "^0.18.0", "@material/mwc-switch": "^0.19.0",
"@material/mwc-tab": "^0.18.0", "@material/mwc-tab": "^0.19.0",
"@material/mwc-tab-bar": "^0.18.0", "@material/mwc-tab-bar": "^0.19.0",
"@material/top-app-bar": "=8.0.0-canary.096a7a066.0", "@material/top-app-bar": "=8.0.0-canary.774dcfc8e.0",
"@mdi/js": "5.6.55", "@mdi/js": "5.6.55",
"@mdi/svg": "5.6.55", "@mdi/svg": "5.6.55",
"@polymer/app-layout": "^3.0.2", "@polymer/app-layout": "^3.0.2",
@ -77,7 +78,7 @@
"@polymer/paper-toast": "^3.0.1", "@polymer/paper-toast": "^3.0.1",
"@polymer/paper-tooltip": "^3.0.1", "@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.1.0", "@polymer/polymer": "3.1.0",
"@thomasloven/round-slider": "0.5.0", "@thomasloven/round-slider": "0.5.2",
"@types/chromecast-caf-sender": "^1.0.3", "@types/chromecast-caf-sender": "^1.0.3",
"@types/sortablejs": "^1.10.6", "@types/sortablejs": "^1.10.6",
"@vaadin/vaadin-combo-box": "^5.0.10", "@vaadin/vaadin-combo-box": "^5.0.10",
@ -88,11 +89,11 @@
"chartjs-chart-timeline": "^0.3.0", "chartjs-chart-timeline": "^0.3.0",
"codemirror": "^5.49.0", "codemirror": "^5.49.0",
"comlink": "^4.3.0", "comlink": "^4.3.0",
"core-js": "^3.6.5",
"cpx": "^1.5.0", "cpx": "^1.5.0",
"cropperjs": "^1.5.7", "cropperjs": "^1.5.7",
"deep-clone-simple": "^1.1.1", "deep-clone-simple": "^1.1.1",
"deep-freeze": "^0.0.1", "deep-freeze": "^0.0.1",
"es6-object-assign": "^1.1.0",
"fecha": "^4.2.0", "fecha": "^4.2.0",
"fuse.js": "^6.0.0", "fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2", "google-timezones-json": "^1.0.2",
@ -103,15 +104,16 @@
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"leaflet": "^1.4.0", "leaflet": "^1.4.0",
"leaflet-draw": "^1.0.4", "leaflet-draw": "^1.0.4",
"lit-element": "^2.3.1", "lit-element": "^2.4.0",
"lit-html": "^1.2.1", "lit-html": "^1.3.0",
"lit-virtualizer": "^0.4.2", "lit-virtualizer": "^0.4.2",
"marked": "^1.1.1", "marked": "^1.1.1",
"mdn-polyfills": "^5.16.0", "mdn-polyfills": "^5.16.0",
"memoize-one": "^5.0.2", "memoize-one": "^5.0.2",
"node-vibrant": "^3.1.5", "node-vibrant": "^3.1.6",
"proxy-polyfill": "^0.3.1", "proxy-polyfill": "^0.3.1",
"punycode": "^2.1.1", "punycode": "^2.1.1",
"qrcode": "^1.4.4",
"regenerator-runtime": "^0.13.2", "regenerator-runtime": "^0.13.2",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"roboto-fontface": "^0.10.0", "roboto-fontface": "^0.10.0",
@ -128,16 +130,18 @@
"xss": "^1.0.6" "xss": "^1.0.6"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.9.0", "@babel/core": "^7.11.6",
"@babel/plugin-external-helpers": "^7.8.3", "@babel/plugin-external-helpers": "^7.10.4",
"@babel/plugin-proposal-class-properties": "^7.8.3", "@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-decorators": "^7.8.3", "@babel/plugin-proposal-decorators": "^7.10.5",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4",
"@babel/plugin-proposal-object-rest-spread": "^7.9.5", "@babel/plugin-proposal-object-rest-spread": "^7.11.0",
"@babel/plugin-proposal-optional-chaining": "^7.9.0", "@babel/plugin-proposal-optional-chaining": "^7.11.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/preset-env": "^7.9.5", "@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/preset-typescript": "^7.9.0", "@babel/plugin-syntax-top-level-await": "^7.10.4",
"@babel/preset-env": "^7.11.5",
"@babel/preset-typescript": "^7.10.4",
"@rollup/plugin-commonjs": "^11.1.0", "@rollup/plugin-commonjs": "^11.1.0",
"@rollup/plugin-json": "^4.0.3", "@rollup/plugin-json": "^4.0.3",
"@rollup/plugin-node-resolve": "^7.1.3", "@rollup/plugin-node-resolve": "^7.1.3",
@ -154,8 +158,8 @@
"@types/mocha": "^7.0.2", "@types/mocha": "^7.0.2",
"@types/resize-observer-browser": "^0.1.3", "@types/resize-observer-browser": "^0.1.3",
"@types/webspeechapi": "^0.0.29", "@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^2.28.0", "@typescript-eslint/eslint-plugin": "^4.4.0",
"@typescript-eslint/parser": "^2.28.0", "@typescript-eslint/parser": "^4.4.0",
"babel-loader": "^8.1.0", "babel-loader": "^8.1.0",
"chai": "^4.2.0", "chai": "^4.2.0",
"del": "^4.0.0", "del": "^4.0.0",
@ -180,7 +184,7 @@
"html-minifier": "^4.0.0", "html-minifier": "^4.0.0",
"husky": "^1.3.1", "husky": "^1.3.1",
"lint-staged": "^8.1.5", "lint-staged": "^8.1.5",
"lit-analyzer": "^1.2.0", "lit-analyzer": "^1.2.1",
"lodash.template": "^4.5.0", "lodash.template": "^4.5.0",
"magic-string": "^0.25.7", "magic-string": "^0.25.7",
"map-stream": "^0.0.7", "map-stream": "^0.0.7",
@ -201,29 +205,24 @@
"source-map-url": "^0.4.0", "source-map-url": "^0.4.0",
"systemjs": "^6.3.2", "systemjs": "^6.3.2",
"terser-webpack-plugin": "^3.0.6", "terser-webpack-plugin": "^3.0.6",
"ts-lit-plugin": "^1.2.0", "ts-lit-plugin": "^1.2.1",
"ts-mocha": "^7.0.0", "ts-mocha": "^7.0.0",
"typescript": "^3.8.3", "typescript": "^4.0.3",
"vinyl-buffer": "^1.0.1", "vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0", "vinyl-source-stream": "^2.0.0",
"webpack": "^4.40.2", "webpack": "5.0.0-rc.3",
"webpack-cli": "^3.3.9", "webpack-cli": "4.0.0-rc.0",
"webpack-dev-server": "^3.10.3", "webpack-dev-server": "^3.10.3",
"webpack-manifest-plugin": "^2.0.4", "webpack-manifest-plugin": "3.0.0-rc.0",
"workbox-build": "^5.1.3", "workbox-build": "^5.1.3"
"worker-plugin": "^4.0.3"
}, },
"_comment": "Polymer fixed to 3.1 because 3.2 throws on logbook page", "_comment": "Polymer fixed to 3.1 because 3.2 throws on logbook page",
"_comment_2": "Fix in https://github.com/Polymer/polymer/pull/5569", "_comment_2": "Fix in https://github.com/Polymer/polymer/pull/5569",
"resolutions": { "resolutions": {
"@webcomponents/webcomponentsjs": "^2.2.10", "@webcomponents/webcomponentsjs": "^2.2.10",
"@polymer/polymer": "3.1.0", "@polymer/polymer": "3.1.0",
"lit-html": "1.2.1", "lit-html": "1.3.0",
"lit-element": "2.3.1", "lit-element": "2.4.0"
"@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"
}, },
"main": "src/home-assistant.js", "main": "src/home-assistant.js",
"husky": { "husky": {

View File

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

View File

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

View File

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

View File

@ -10,10 +10,7 @@ export const dynamicElement = directive(
let element = part.value as HTMLElement | undefined; let element = part.value as HTMLElement | undefined;
if ( if (tag === element?.localName) {
element !== undefined &&
tag.toUpperCase() === (element as HTMLElement).tagName
) {
if (properties) { if (properties) {
Object.entries(properties).forEach(([key, value]) => { Object.entries(properties).forEach(([key, value]) => {
element![key] = value; element![key] = value;

View File

@ -23,7 +23,7 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
case "problem": case "problem":
case "safety": case "safety":
case "smoke": case "smoke":
return is_off ? "hass:shield-check" : "hass:alert"; return is_off ? "hass:check-circle" : "hass:alert-circle";
case "heat": case "heat":
return is_off ? "hass:thermometer" : "hass:fire"; return is_off ? "hass:thermometer" : "hass:fire";
case "light": case "light":

View File

@ -51,21 +51,17 @@ class SearchInput extends LitElement {
@value-changed=${this._filterInputChanged} @value-changed=${this._filterInputChanged}
.noLabelFloat=${this.noLabelFloat} .noLabelFloat=${this.noLabelFloat}
> >
<ha-svg-icon <slot name="prefix" slot="prefix">
path=${mdiMagnify} <ha-svg-icon class="prefix" .path=${mdiMagnify}></ha-svg-icon>
slot="prefix" </slot>
class="prefix"
></ha-svg-icon>
${this.filter && ${this.filter &&
html` html`
<mwc-icon-button <mwc-icon-button
slot="suffix" slot="suffix"
class="suffix"
@click=${this._clearSearch} @click=${this._clearSearch}
alt="Clear"
title="Clear" title="Clear"
> >
<ha-svg-icon path=${mdiClose}></ha-svg-icon> <ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button> </mwc-icon-button>
`} `}
</paper-input> </paper-input>

View File

@ -0,0 +1,244 @@
// MIT License
// Copyright (c) 2015 - present Microsoft Corporation
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/
/**
* An inlined enum containing useful character codes (to be used with String.charCodeAt).
* Please leave the const keyword such that it gets inlined when compiled to JavaScript!
*/
export enum CharCode {
Null = 0,
/**
* The `\b` character.
*/
Backspace = 8,
/**
* The `\t` character.
*/
Tab = 9,
/**
* The `\n` character.
*/
LineFeed = 10,
/**
* The `\r` character.
*/
CarriageReturn = 13,
Space = 32,
/**
* The `!` character.
*/
ExclamationMark = 33,
/**
* The `"` character.
*/
DoubleQuote = 34,
/**
* The `#` character.
*/
Hash = 35,
/**
* The `$` character.
*/
DollarSign = 36,
/**
* The `%` character.
*/
PercentSign = 37,
/**
* The `&` character.
*/
Ampersand = 38,
/**
* The `'` character.
*/
SingleQuote = 39,
/**
* The `(` character.
*/
OpenParen = 40,
/**
* The `)` character.
*/
CloseParen = 41,
/**
* The `*` character.
*/
Asterisk = 42,
/**
* The `+` character.
*/
Plus = 43,
/**
* The `,` character.
*/
Comma = 44,
/**
* The `-` character.
*/
Dash = 45,
/**
* The `.` character.
*/
Period = 46,
/**
* The `/` character.
*/
Slash = 47,
Digit0 = 48,
Digit1 = 49,
Digit2 = 50,
Digit3 = 51,
Digit4 = 52,
Digit5 = 53,
Digit6 = 54,
Digit7 = 55,
Digit8 = 56,
Digit9 = 57,
/**
* The `:` character.
*/
Colon = 58,
/**
* The `;` character.
*/
Semicolon = 59,
/**
* The `<` character.
*/
LessThan = 60,
/**
* The `=` character.
*/
Equals = 61,
/**
* The `>` character.
*/
GreaterThan = 62,
/**
* The `?` character.
*/
QuestionMark = 63,
/**
* The `@` character.
*/
AtSign = 64,
A = 65,
B = 66,
C = 67,
D = 68,
E = 69,
F = 70,
G = 71,
H = 72,
I = 73,
J = 74,
K = 75,
L = 76,
M = 77,
N = 78,
O = 79,
P = 80,
Q = 81,
R = 82,
S = 83,
T = 84,
U = 85,
V = 86,
W = 87,
X = 88,
Y = 89,
Z = 90,
/**
* The `[` character.
*/
OpenSquareBracket = 91,
/**
* The `\` character.
*/
Backslash = 92,
/**
* The `]` character.
*/
CloseSquareBracket = 93,
/**
* The `^` character.
*/
Caret = 94,
/**
* The `_` character.
*/
Underline = 95,
/**
* The ``(`)`` character.
*/
BackTick = 96,
a = 97,
b = 98,
c = 99,
d = 100,
e = 101,
f = 102,
g = 103,
h = 104,
i = 105,
j = 106,
k = 107,
l = 108,
m = 109,
n = 110,
o = 111,
p = 112,
q = 113,
r = 114,
s = 115,
t = 116,
u = 117,
v = 118,
w = 119,
x = 120,
y = 121,
z = 122,
/**
* The `{` character.
*/
OpenCurlyBrace = 123,
/**
* The `|` character.
*/
Pipe = 124,
/**
* The `}` character.
*/
CloseCurlyBrace = 125,
/**
* The `~` character.
*/
Tilde = 126,
}

View File

@ -0,0 +1,463 @@
/* eslint-disable no-console */
// MIT License
// Copyright (c) 2015 - present Microsoft Corporation
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { CharCode } from "./char-code";
const _debug = false;
export interface Match {
start: number;
end: number;
}
const _maxLen = 128;
function initTable() {
const table: number[][] = [];
const row: number[] = [0];
for (let i = 1; i <= _maxLen; i++) {
row.push(-i);
}
for (let i = 0; i <= _maxLen; i++) {
const thisRow = row.slice(0);
thisRow[0] = -i;
table.push(thisRow);
}
return table;
}
function isSeparatorAtPos(value: string, index: number): boolean {
if (index < 0 || index >= value.length) {
return false;
}
const code = value.charCodeAt(index);
switch (code) {
case CharCode.Underline:
case CharCode.Dash:
case CharCode.Period:
case CharCode.Space:
case CharCode.Slash:
case CharCode.Backslash:
case CharCode.SingleQuote:
case CharCode.DoubleQuote:
case CharCode.Colon:
case CharCode.DollarSign:
return true;
default:
return false;
}
}
function isWhitespaceAtPos(value: string, index: number): boolean {
if (index < 0 || index >= value.length) {
return false;
}
const code = value.charCodeAt(index);
switch (code) {
case CharCode.Space:
case CharCode.Tab:
return true;
default:
return false;
}
}
function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean {
return word[pos] !== wordLow[pos];
}
function isPatternInWord(
patternLow: string,
patternPos: number,
patternLen: number,
wordLow: string,
wordPos: number,
wordLen: number
): boolean {
while (patternPos < patternLen && wordPos < wordLen) {
if (patternLow[patternPos] === wordLow[wordPos]) {
patternPos += 1;
}
wordPos += 1;
}
return patternPos === patternLen; // pattern must be exhausted
}
enum Arrow {
Top = 0b1,
Diag = 0b10,
Left = 0b100,
}
/**
* A tuple of three values.
* 0. the score
* 1. the matches encoded as bitmask (2^53)
* 2. the offset at which matching started
*/
export type FuzzyScore = [number, number, number];
interface FilterGlobals {
_matchesCount: number;
_topMatch2: number;
_topScore: number;
_wordStart: number;
_firstMatchCanBeWeak: boolean;
_table: number[][];
_scores: number[][];
_arrows: Arrow[][];
}
function initGlobals(): FilterGlobals {
return {
_matchesCount: 0,
_topMatch2: 0,
_topScore: 0,
_wordStart: 0,
_firstMatchCanBeWeak: false,
_table: initTable(),
_scores: initTable(),
_arrows: <Arrow[][]>initTable(),
};
}
export function fuzzyScore(
pattern: string,
patternLow: string,
patternStart: number,
word: string,
wordLow: string,
wordStart: number,
firstMatchCanBeWeak: boolean
): FuzzyScore | undefined {
const globals = initGlobals();
const patternLen = pattern.length > _maxLen ? _maxLen : pattern.length;
const wordLen = word.length > _maxLen ? _maxLen : word.length;
if (
patternStart >= patternLen ||
wordStart >= wordLen ||
patternLen - patternStart > wordLen - wordStart
) {
return undefined;
}
// Run a simple check if the characters of pattern occur
// (in order) at all in word. If that isn't the case we
// stop because no match will be possible
if (
!isPatternInWord(
patternLow,
patternStart,
patternLen,
wordLow,
wordStart,
wordLen
)
) {
return undefined;
}
let row = 1;
let column = 1;
let patternPos = patternStart;
let wordPos = wordStart;
let hasStrongFirstMatch = false;
// There will be a match, fill in tables
for (
row = 1, patternPos = patternStart;
patternPos < patternLen;
row++, patternPos++
) {
for (
column = 1, wordPos = wordStart;
wordPos < wordLen;
column++, wordPos++
) {
const score = _doScore(
pattern,
patternLow,
patternPos,
patternStart,
word,
wordLow,
wordPos
);
if (patternPos === patternStart && score > 1) {
hasStrongFirstMatch = true;
}
globals._scores[row][column] = score;
const diag =
globals._table[row - 1][column - 1] + (score > 1 ? 1 : score);
const top = globals._table[row - 1][column] + -1;
const left = globals._table[row][column - 1] + -1;
if (left >= top) {
// left or diag
if (left > diag) {
globals._table[row][column] = left;
globals._arrows[row][column] = Arrow.Left;
} else if (left === diag) {
globals._table[row][column] = left;
globals._arrows[row][column] = Arrow.Left || Arrow.Diag;
} else {
globals._table[row][column] = diag;
globals._arrows[row][column] = Arrow.Diag;
}
} else if (top > diag) {
globals._table[row][column] = top;
globals._arrows[row][column] = Arrow.Top;
} else if (top === diag) {
globals._table[row][column] = top;
globals._arrows[row][column] = Arrow.Top || Arrow.Diag;
} else {
globals._table[row][column] = diag;
globals._arrows[row][column] = Arrow.Diag;
}
}
}
if (_debug) {
printTables(pattern, patternStart, word, wordStart, globals);
}
if (!hasStrongFirstMatch && !firstMatchCanBeWeak) {
return undefined;
}
globals._matchesCount = 0;
globals._topScore = -100;
globals._wordStart = wordStart;
globals._firstMatchCanBeWeak = firstMatchCanBeWeak;
_findAllMatches2(
row - 1,
column - 1,
patternLen === wordLen ? 1 : 0,
0,
false,
globals
);
if (globals._matchesCount === 0) {
return undefined;
}
return [globals._topScore, globals._topMatch2, wordStart];
}
function _doScore(
pattern: string,
patternLow: string,
patternPos: number,
patternStart: number,
word: string,
wordLow: string,
wordPos: number
) {
if (patternLow[patternPos] !== wordLow[wordPos]) {
return -1;
}
if (wordPos === patternPos - patternStart) {
// common prefix: `foobar <-> foobaz`
// ^^^^^
if (pattern[patternPos] === word[wordPos]) {
return 7;
}
return 5;
}
if (
isUpperCaseAtPos(wordPos, word, wordLow) &&
(wordPos === 0 || !isUpperCaseAtPos(wordPos - 1, word, wordLow))
) {
// hitting upper-case: `foo <-> forOthers`
// ^^ ^
if (pattern[patternPos] === word[wordPos]) {
return 7;
}
return 5;
}
if (
isSeparatorAtPos(wordLow, wordPos) &&
(wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1))
) {
// hitting a separator: `. <-> foo.bar`
// ^
return 5;
}
if (
isSeparatorAtPos(wordLow, wordPos - 1) ||
isWhitespaceAtPos(wordLow, wordPos - 1)
) {
// post separator: `foo <-> bar_foo`
// ^^^
return 5;
}
return 1;
}
function printTable(
table: number[][],
pattern: string,
patternLen: number,
word: string,
wordLen: number
): string {
function pad(s: string, n: number, _pad = " ") {
while (s.length < n) {
s = _pad + s;
}
return s;
}
let ret = ` | |${word
.split("")
.map((c) => pad(c, 3))
.join("|")}\n`;
for (let i = 0; i <= patternLen; i++) {
if (i === 0) {
ret += " |";
} else {
ret += `${pattern[i - 1]}|`;
}
ret +=
table[i]
.slice(0, wordLen + 1)
.map((n) => pad(n.toString(), 3))
.join("|") + "\n";
}
return ret;
}
function printTables(
pattern: string,
patternStart: number,
word: string,
wordStart: number,
globals: FilterGlobals
): void {
pattern = pattern.substr(patternStart);
word = word.substr(wordStart);
console.log(
printTable(globals._table, pattern, pattern.length, word, word.length)
);
console.log(
printTable(globals._arrows, pattern, pattern.length, word, word.length)
);
console.log(
printTable(globals._scores, pattern, pattern.length, word, word.length)
);
}
function _findAllMatches2(
row: number,
column: number,
total: number,
matches: number,
lastMatched: boolean,
globals: FilterGlobals
): void {
if (globals._matchesCount >= 10 || total < -25) {
// stop when having already 10 results, or
// when a potential alignment as already 5 gaps
return;
}
let simpleMatchCount = 0;
while (row > 0 && column > 0) {
const score = globals._scores[row][column];
const arrow = globals._arrows[row][column];
if (arrow === Arrow.Left) {
// left -> no match, skip a word character
column -= 1;
if (lastMatched) {
total -= 5; // new gap penalty
} else if (matches !== 0) {
total -= 1; // gap penalty after first match
}
lastMatched = false;
simpleMatchCount = 0;
} else if (arrow && Arrow.Diag) {
if (arrow && Arrow.Left) {
// left
_findAllMatches2(
row,
column - 1,
matches !== 0 ? total - 1 : total, // gap penalty after first match
matches,
lastMatched,
globals
);
}
// diag
total += score;
row -= 1;
column -= 1;
lastMatched = true;
// match -> set a 1 at the word pos
matches += 2 ** (column + globals._wordStart);
// count simple matches and boost a row of
// simple matches when they yield in a
// strong match.
if (score === 1) {
simpleMatchCount += 1;
if (row === 0 && !globals._firstMatchCanBeWeak) {
// when the first match is a weak
// match we discard it
return;
}
} else {
// boost
total += 1 + simpleMatchCount * (score - 1);
simpleMatchCount = 0;
}
} else {
return;
}
}
total -= column >= 3 ? 9 : column * 3; // late start penalty
// dynamically keep track of the current top score
// and insert the current best score at head, the rest at tail
globals._matchesCount += 1;
if (total > globals._topScore) {
globals._topScore = total;
globals._topMatch2 = matches;
}
}
// #endregion

View File

@ -0,0 +1,66 @@
import { fuzzyScore } from "./filter";
/**
* Determine whether a sequence of letters exists in another string,
* in that order, allowing for skipping. Ex: "chdr" exists in "chandelier")
*
* @param {string} filter - Sequence of letters to check for
* @param {string} word - Word to check for sequence
*
* @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
*/
export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
let topScore = 0;
for (const word of words) {
const scores = fuzzyScore(
filter,
filter.toLowerCase(),
0,
word,
word.toLowerCase(),
0,
true
);
if (!scores) {
continue;
}
// The VS Code implementation of filter treats a score of "0" as just barely a match
// But we will typically use this matcher in a .filter(), which interprets 0 as a failure.
// By shifting all scores up by 1, we allow "0" matches, while retaining score precedence
const score = scores[0] + 1;
if (score > topScore) {
topScore = score;
}
}
return topScore;
};
export interface ScorableTextItem {
score?: number;
text: string;
altText?: string;
}
type FuzzyFilterSort = <T extends ScorableTextItem>(
filter: string,
items: T[]
) => T[];
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
return items
.map((item) => {
item.score = item.altText
? fuzzySequentialMatch(filter, item.text, item.altText)
: fuzzySequentialMatch(filter, item.text);
return item;
})
.filter((item) => item.score === undefined || item.score > 0)
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
);
};

View File

@ -1,4 +1,5 @@
import IntlMessageFormat from "intl-messageformat"; import IntlMessageFormat from "intl-messageformat";
import { shouldPolyfill } from "@formatjs/intl-pluralrules/should-polyfill";
import { Resources } from "../../types"; import { Resources } from "../../types";
export type LocalizeFunc = (key: string, ...args: any[]) => string; export type LocalizeFunc = (key: string, ...args: any[]) => string;
@ -12,8 +13,8 @@ export interface FormatsType {
time: FormatType; time: FormatType;
} }
if (!Intl.PluralRules) { if (shouldPolyfill()) {
import("@formatjs/intl-pluralrules/polyfill-locales"); await import("@formatjs/intl-pluralrules/polyfill-locales");
} }
/** /**

View File

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

View File

@ -2,7 +2,10 @@ import { Connection, UnsubscribeFunc } from "home-assistant-js-websocket";
export const subscribeOne = async <T>( export const subscribeOne = async <T>(
conn: Connection, conn: Connection,
subscribe: (conn: Connection, onChange: (items: T) => void) => UnsubscribeFunc subscribe: (
conn2: Connection,
onChange: (items: T) => void
) => UnsubscribeFunc
) => ) =>
new Promise<T>((resolve) => { new Promise<T>((resolve) => {
const unsub = subscribe(conn, (items) => { 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; // 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 // 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. // `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, func: T,
wait: number, wait: number,
leading = true, leading = true,

View File

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

View File

@ -73,13 +73,17 @@ export interface DataTableColumnData extends DataTableSortColumnData {
hidden?: boolean; hidden?: boolean;
} }
type ClonedDataTableColumnData = Omit<DataTableColumnData, "title"> & {
title?: string;
};
export interface DataTableRowData { export interface DataTableRowData {
[key: string]: any; [key: string]: any;
selectable?: boolean; selectable?: boolean;
} }
export interface SortableColumnContainer { export interface SortableColumnContainer {
[key: string]: DataTableSortColumnData; [key: string]: ClonedDataTableColumnData;
} }
@customElement("ha-data-table") @customElement("ha-data-table")
@ -90,6 +94,8 @@ export class HaDataTable extends LitElement {
@property({ type: Boolean }) public selectable = false; @property({ type: Boolean }) public selectable = false;
@property({ type: Boolean }) public clickable = false;
@property({ type: Boolean }) public hasFab = false; @property({ type: Boolean }) public hasFab = false;
@property({ type: Boolean, attribute: "auto-height" }) @property({ type: Boolean, attribute: "auto-height" })
@ -101,6 +107,9 @@ export class HaDataTable extends LitElement {
@property({ type: String }) public searchLabel?: string; @property({ type: String }) public searchLabel?: string;
@property({ type: Boolean, attribute: "no-label-float" })
public noLabelFloat? = false;
@property({ type: String }) public filter = ""; @property({ type: String }) public filter = "";
@internalProperty() private _filterable = false; @internalProperty() private _filterable = false;
@ -113,9 +122,9 @@ export class HaDataTable extends LitElement {
@internalProperty() private _filteredData: DataTableRowData[] = []; @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; private _checkableRowsCount?: number;
@ -166,11 +175,13 @@ export class HaDataTable extends LitElement {
} }
const clonedColumns: DataTableColumnContainer = deepClone(this.columns); const clonedColumns: DataTableColumnContainer = deepClone(this.columns);
Object.values(clonedColumns).forEach((column: DataTableColumnData) => { Object.values(clonedColumns).forEach(
(column: ClonedDataTableColumnData) => {
delete column.title; delete column.title;
delete column.type; delete column.type;
delete column.template; delete column.template;
}); }
);
this._sortColumns = clonedColumns; this._sortColumns = clonedColumns;
} }
@ -206,6 +217,7 @@ export class HaDataTable extends LitElement {
<search-input <search-input
@value-changed=${this._handleSearchChange} @value-changed=${this._handleSearchChange}
.label=${this.searchLabel} .label=${this.searchLabel}
.noLabelFloat=${this.noLabelFloat}
></search-input> ></search-input>
</div> </div>
` `
@ -220,7 +232,7 @@ export class HaDataTable extends LitElement {
style=${styleMap({ style=${styleMap({
height: this.autoHeight height: this.autoHeight
? `${(this._filteredData.length || 1) * 53 + 57}px` ? `${(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"> <div class="mdc-data-table__header-row" role="row">
@ -317,12 +329,13 @@ export class HaDataTable extends LitElement {
<div <div
aria-rowindex=${index} aria-rowindex=${index}
role="row" role="row"
.rowId="${row[this.id]}" .rowId=${row[this.id]}
@click=${this._handleRowClick} @click=${this._handleRowClick}
class="mdc-data-table__row ${classMap({ class="mdc-data-table__row ${classMap({
"mdc-data-table__row--selected": this._checkedRows.includes( "mdc-data-table__row--selected": this._checkedRows.includes(
String(row[this.id]) String(row[this.id])
), ),
clickable: this.clickable,
})}" })}"
aria-selected=${ifDefined( aria-selected=${ifDefined(
this._checkedRows.includes(String(row[this.id])) this._checkedRows.includes(String(row[this.id]))
@ -340,6 +353,7 @@ export class HaDataTable extends LitElement {
<ha-checkbox <ha-checkbox
class="mdc-data-table__row-checkbox" class="mdc-data-table__row-checkbox"
@change=${this._handleRowCheckboxClick} @change=${this._handleRowCheckboxClick}
.rowId=${row[this.id]}
.disabled=${row.selectable === false} .disabled=${row.selectable === false}
.checked=${this._checkedRows.includes( .checked=${this._checkedRows.includes(
String(row[this.id]) String(row[this.id])
@ -447,9 +461,7 @@ export class HaDataTable extends LitElement {
); );
private _handleHeaderClick(ev: Event) { private _handleHeaderClick(ev: Event) {
const columnId = ((ev.target as HTMLElement).closest( const columnId = (ev.currentTarget as any).columnId;
".mdc-data-table__header-cell"
) as any).columnId;
if (!this.columns[columnId].sortable) { if (!this.columns[columnId].sortable) {
return; return;
} }
@ -483,8 +495,8 @@ export class HaDataTable extends LitElement {
} }
private _handleRowCheckboxClick(ev: Event) { private _handleRowCheckboxClick(ev: Event) {
const checkbox = ev.target as HaCheckbox; const checkbox = ev.currentTarget as HaCheckbox;
const rowId = (checkbox.closest(".mdc-data-table__row") as any).rowId; const rowId = (checkbox as any).rowId;
if (checkbox.checked) { if (checkbox.checked) {
if (this._checkedRows.includes(rowId)) { if (this._checkedRows.includes(rowId)) {
@ -502,7 +514,7 @@ export class HaDataTable extends LitElement {
if (target.tagName === "HA-CHECKBOX") { if (target.tagName === "HA-CHECKBOX") {
return; return;
} }
const rowId = (target.closest(".mdc-data-table__row") as any).rowId; const rowId = (ev.currentTarget as any).rowId;
fireEvent(this, "row-click", { id: rowId }, { bubbles: false }); fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
} }
@ -523,7 +535,7 @@ export class HaDataTable extends LitElement {
return; return;
} }
await this.updateComplete; await this.updateComplete;
this._table.style.height = `calc(100% - ${this._header.clientHeight}px)`; this._headerHeight = this._header.clientHeight;
} }
@eventOptions({ passive: true }) @eventOptions({ passive: true })
@ -876,6 +888,9 @@ export class HaDataTable extends LitElement {
.forceLTR { .forceLTR {
direction: ltr; direction: ltr;
} }
.clickable {
cursor: pointer;
}
`; `;
} }
} }

View File

@ -16,7 +16,7 @@ export const filterData = async (
filter: FilterDataParamTypes[2] filter: FilterDataParamTypes[2]
): Promise<ReturnType<FilterDataType>> => { ): Promise<ReturnType<FilterDataType>> => {
if (!worker) { 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); return await worker.filterData(data, columns, filter);
@ -29,7 +29,7 @@ export const sortData = async (
sortColumn: SortDataParamTypes[3] sortColumn: SortDataParamTypes[3]
): Promise<ReturnType<SortDataType>> => { ): Promise<ReturnType<SortDataType>> => {
if (!worker) { 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); return await worker.sortData(data, columns, direction, sortColumn);

View File

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

View File

@ -9,9 +9,17 @@ import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-action-picker") @customElement("ha-device-action-picker")
class HaDeviceActionPicker extends HaDeviceAutomationPicker<DeviceAction> { class HaDeviceActionPicker extends HaDeviceAutomationPicker<DeviceAction> {
protected NO_AUTOMATION_TEXT = "No actions"; protected get NO_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.actions.no_actions"
);
}
protected UNKNOWN_AUTOMATION_TEXT = "Unknown action"; protected get UNKNOWN_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.actions.unknown_action"
);
}
constructor() { constructor() {
super( super(

View File

@ -33,16 +33,24 @@ export abstract class HaDeviceAutomationPicker<
@property() public value?: T; @property() public value?: T;
protected NO_AUTOMATION_TEXT = "No automations";
protected UNKNOWN_AUTOMATION_TEXT = "Unknown automation";
@internalProperty() private _automations: T[] = []; @internalProperty() private _automations: T[] = [];
// Trigger an empty render so we start with a clean DOM. // Trigger an empty render so we start with a clean DOM.
// paper-listbox does not like changing things around. // paper-listbox does not like changing things around.
@internalProperty() private _renderEmpty = false; @internalProperty() private _renderEmpty = false;
protected get NO_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.actions.no_actions"
);
}
protected get UNKNOWN_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.actions.unknown_action"
);
}
private _localizeDeviceAutomation: ( private _localizeDeviceAutomation: (
hass: HomeAssistant, hass: HomeAssistant,
automation: T automation: T

View File

@ -11,9 +11,17 @@ import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
class HaDeviceConditionPicker extends HaDeviceAutomationPicker< class HaDeviceConditionPicker extends HaDeviceAutomationPicker<
DeviceCondition DeviceCondition
> { > {
protected NO_AUTOMATION_TEXT = "No conditions"; protected get NO_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.conditions.no_conditions"
);
}
protected UNKNOWN_AUTOMATION_TEXT = "Unknown condition"; protected get UNKNOWN_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.conditions.unknown_condition"
);
}
constructor() { constructor() {
super( super(

View File

@ -9,9 +9,17 @@ import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-trigger-picker") @customElement("ha-device-trigger-picker")
class HaDeviceTriggerPicker extends HaDeviceAutomationPicker<DeviceTrigger> { class HaDeviceTriggerPicker extends HaDeviceAutomationPicker<DeviceTrigger> {
protected NO_AUTOMATION_TEXT = "No triggers"; protected get NO_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.triggers.no_triggers"
);
}
protected UNKNOWN_AUTOMATION_TEXT = "Unknown trigger"; protected get UNKNOWN_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.triggers.unknown_trigger"
);
}
constructor() { constructor() {
super( super(

View File

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

View File

@ -1,3 +1,4 @@
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light"; import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
@ -16,8 +17,9 @@ import {
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { PolymerChangedEvent } from "../../polymer-types"; import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-icon-button"; import "../ha-svg-icon";
import "./state-badge"; import "./state-badge";
import "@material/mwc-icon-button/mwc-icon-button";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
@ -55,7 +57,7 @@ class HaEntityAttributePicker extends LitElement {
@property({ type: Boolean }) private _opened = false; @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) { protected shouldUpdate(changedProps: PropertyValues) {
return !(!changedProps.has("_opened") && this._opened); return !(!changedProps.has("_opened") && this._opened);
@ -80,6 +82,7 @@ class HaEntityAttributePicker extends LitElement {
.value=${this._value} .value=${this._value}
.allowCustomValue=${this.allowCustomValue} .allowCustomValue=${this.allowCustomValue}
.renderer=${rowRenderer} .renderer=${rowRenderer}
attr-for-value="bind-value"
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
> >
@ -97,33 +100,35 @@ class HaEntityAttributePicker extends LitElement {
autocorrect="off" autocorrect="off"
spellcheck="false" spellcheck="false"
> >
<div class="suffix" slot="suffix">
${this.value ${this.value
? html` ? html`
<ha-icon-button <mwc-icon-button
aria-label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.entity.entity-picker.clear" "ui.components.entity.entity-picker.clear"
)} )}
slot="suffix"
class="clear-button" class="clear-button"
icon="hass:close" tabindex="-1"
@click=${this._clearValue} @click=${this._clearValue}
no-ripple no-ripple
> >
Clear <ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</ha-icon-button> </mwc-icon-button>
` `
: ""} : ""}
<ha-icon-button <mwc-icon-button
aria-label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.entity.entity-attribute-picker.show_attributes" "ui.components.entity.entity-attribute-picker.show_attributes"
)} )}
slot="suffix"
class="toggle-button" class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"} tabindex="-1"
> >
Toggle <ha-svg-icon
</ha-icon-button> .path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon>
</mwc-icon-button>
</div>
</paper-input> </paper-input>
</vaadin-combo-box-light> </vaadin-combo-box-light>
`; `;
@ -159,7 +164,10 @@ class HaEntityAttributePicker extends LitElement {
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
paper-input > ha-icon-button { .suffix {
display: flex;
}
mwc-icon-button {
--mdc-icon-button-size: 24px; --mdc-icon-button-size: 24px;
padding: 0px 2px; padding: 0px 2px;
color: var(--secondary-text-color); color: var(--secondary-text-color);

View File

@ -1,3 +1,5 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
@ -20,7 +22,7 @@ import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import { PolymerChangedEvent } from "../../polymer-types"; import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-icon-button"; import "../ha-svg-icon";
import "./state-badge"; import "./state-badge";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
@ -97,10 +99,12 @@ export class HaEntityPicker extends LitElement {
@property({ type: Boolean }) private _opened = false; @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; private _initedStates = false;
private _states: HassEntity[] = [];
private _getStates = memoizeOne( private _getStates = memoizeOne(
( (
_opened: boolean, _opened: boolean,
@ -166,7 +170,7 @@ export class HaEntityPicker extends LitElement {
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
if (!this._initedStates || (changedProps.has("_opened") && this._opened)) { if (!this._initedStates || (changedProps.has("_opened") && this._opened)) {
const states = this._getStates( this._states = this._getStates(
this._opened, this._opened,
this.hass, this.hass,
this.includeDomains, this.includeDomains,
@ -174,7 +178,7 @@ export class HaEntityPicker extends LitElement {
this.entityFilter, this.entityFilter,
this.includeDeviceClasses this.includeDeviceClasses
); );
(this._comboBox as any).items = states; (this._comboBox as any).filteredItems = this._states;
this._initedStates = true; this._initedStates = true;
} }
} }
@ -192,6 +196,7 @@ export class HaEntityPicker extends LitElement {
.renderer=${rowRenderer} .renderer=${rowRenderer}
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged}
> >
<paper-input <paper-input
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
@ -206,33 +211,35 @@ export class HaEntityPicker extends LitElement {
autocorrect="off" autocorrect="off"
spellcheck="false" spellcheck="false"
> >
<div class="suffix" slot="suffix">
${this.value && !this.hideClearIcon ${this.value && !this.hideClearIcon
? html` ? html`
<ha-icon-button <mwc-icon-button
aria-label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.entity.entity-picker.clear" "ui.components.entity.entity-picker.clear"
)} )}
slot="suffix"
class="clear-button" class="clear-button"
icon="hass:close" tabindex="-1"
@click=${this._clearValue} @click=${this._clearValue}
no-ripple no-ripple
> >
Clear <ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</ha-icon-button> </mwc-icon-button>
` `
: ""} : ""}
<ha-icon-button <mwc-icon-button
aria-label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.entity.entity-picker.show_entities" "ui.components.entity.entity-picker.show_entities"
)} )}
slot="suffix"
class="toggle-button" class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"} tabindex="-1"
> >
Toggle <ha-svg-icon
</ha-icon-button> .path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon>
</mwc-icon-button>
</div>
</paper-input> </paper-input>
</vaadin-combo-box-light> </vaadin-combo-box-light>
`; `;
@ -258,6 +265,15 @@ export class HaEntityPicker extends LitElement {
} }
} }
private _filterChanged(ev: CustomEvent): void {
const filterString = ev.detail.value.toLowerCase();
(this._comboBox as any).filteredItems = this._states.filter(
(state) =>
state.entity_id.toLowerCase().includes(filterString) ||
computeStateName(state).toLowerCase().includes(filterString)
);
}
private _setValue(value: string) { private _setValue(value: string) {
this.value = value; this.value = value;
setTimeout(() => { setTimeout(() => {
@ -268,7 +284,10 @@ export class HaEntityPicker extends LitElement {
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
paper-input > ha-icon-button { .suffix {
display: flex;
}
mwc-icon-button {
--mdc-icon-button-size: 24px; --mdc-icon-button-size: 24px;
padding: 0px 2px; padding: 0px 2px;
color: var(--secondary-text-color); color: var(--secondary-text-color);

View File

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

View File

@ -11,11 +11,14 @@ import {
} from "lit-element"; } from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined"; import { ifDefined } from "lit-html/directives/if-defined";
import { styleMap } from "lit-html/directives/style-map"; import { styleMap } from "lit-html/directives/style-map";
import { computeActiveState } from "../../common/entity/compute_active_state"; import { computeActiveState } from "../../common/entity/compute_active_state";
import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { stateIcon } from "../../common/entity/state_icon"; import { stateIcon } from "../../common/entity/state_icon";
import { iconColorCSS } from "../../common/style/icon_color_css"; import { iconColorCSS } from "../../common/style/icon_color_css";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../ha-icon"; import "../ha-icon";
export class StateBadge extends LitElement { export class StateBadge extends LitElement {
@ -37,7 +40,13 @@ export class StateBadge extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
const stateObj = this.stateObj; const stateObj = this.stateObj;
if (!stateObj || !this._showIcon) { if (!stateObj) {
return html`<div class="missing">
<ha-icon icon="hass:alert"></ha-icon>
</div>`;
}
if (!this._showIcon) {
return html``; return html``;
} }
@ -140,6 +149,9 @@ export class StateBadge extends LitElement {
ha-icon { ha-icon {
transition: color 0.3s ease-in-out, filter 0.3s ease-in-out; transition: color 0.3s ease-in-out, filter 0.3s ease-in-out;
} }
.missing {
color: #fce588;
}
${iconColorCSS} ${iconColorCSS}
`; `;

View File

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

View File

@ -34,8 +34,8 @@ export class HaBar extends LitElement {
return svg` return svg`
<svg> <svg>
<g> <g>
<rect></rect> <rect/>
<rect width="${valuePrecentage}%"></rect> <rect width="${valuePrecentage}%"/>
</g> </g>
</svg> </svg>
`; `;
@ -43,6 +43,9 @@ export class HaBar extends LitElement {
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
rect {
height: 100%;
}
rect:first-child { rect:first-child {
width: 100%; width: 100%;
fill: var(--ha-bar-background-color, var(--secondary-background-color)); 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; @property({ type: Boolean }) public disabled = false;
@query("mwc-menu") private _menu?: Menu; @query("mwc-menu", true) private _menu?: Menu;
public get items() { public get items() {
return this._menu?.items; return this._menu?.items;
@ -62,6 +62,9 @@ export class HaButtonMenu extends LitElement {
display: inline-block; display: inline-block;
position: relative; position: relative;
} }
::slotted([disabled]) {
color: var(--disabled-text-color);
}
`; `;
} }
} }

View File

@ -50,9 +50,12 @@ export class HaCard extends LitElement {
font-family: var(--ha-card-header-font-family, inherit); font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px); font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em; letter-spacing: -0.012em;
line-height: 32px; line-height: 48px;
padding: 24px 16px 16px; padding: 12px 16px 16px;
display: block; display: block;
margin-block-start: 0px;
margin-block-end: 0px;
font-weight: normal;
} }
:host ::slotted(.card-content:not(:first-child)), :host ::slotted(.card-content:not(:first-child)),
@ -75,7 +78,7 @@ export class HaCard extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
${this.header ${this.header
? html` <div class="card-header">${this.header}</div> ` ? html`<h1 class="card-header">${this.header}</h1>`
: html``} : html``}
<slot></slot> <slot></slot>
`; `;

View File

@ -1,20 +1,9 @@
// @ts-ignore import { customElement, property } from "lit-element";
import progressStyles from "@material/circular-progress/dist/mdc.circular-progress.min.css"; import { CircularProgress } from "@material/mwc-circular-progress";
import {
css,
customElement,
html,
LitElement,
property,
svg,
SVGTemplateResult,
TemplateResult,
unsafeCSS,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
@customElement("ha-circular-progress") @customElement("ha-circular-progress")
export class HaCircularProgress extends LitElement { // @ts-ignore
export class HaCircularProgress extends CircularProgress {
@property({ type: Boolean }) @property({ type: Boolean })
public active = false; public active = false;
@ -24,65 +13,31 @@ export class HaCircularProgress extends LitElement {
@property() @property()
public size: "small" | "medium" | "large" = "medium"; public size: "small" | "medium" | "large" = "medium";
protected render(): TemplateResult { // @ts-ignore
let indeterminatePart: SVGTemplateResult; public set density(_) {
// just a dummy
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 public get density() {
// We have not implemented the determinate support of mdc circular progress. switch (this.size) {
// prettier-ignore case "small":
return html` return -5;
<div case "medium":
class="mdc-circular-progress ${classMap({ return 0;
"mdc-circular-progress--indeterminate": this.active, case "large":
[`mdc-circular-progress--${this.size}`]: true, return 5;
})}" default:
role="progressbar" return 0;
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>
`;
} }
static get styles() { // @ts-ignore
return [ public set indeterminate(_) {
unsafeCSS(progressStyles), // just a dummy
css`
:host {
text-align: initial;
} }
`,
]; public get indeterminate() {
return this.active;
} }
} }

View File

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

View File

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

View File

@ -0,0 +1,119 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
query,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-svg-icon";
import { mdiChevronDown } from "@mdi/js";
import { classMap } from "lit-html/directives/class-map";
@customElement("ha-expansion-panel")
class HaExpansionPanel extends LitElement {
@property({ type: Boolean, reflect: true }) expanded = false;
@property({ type: Boolean, reflect: true }) outlined = false;
@query(".container") private _container!: HTMLDivElement;
protected render(): TemplateResult {
return html`
<div class="summary" @click=${this._toggleContainer}>
<slot name="title"></slot>
<ha-svg-icon
.path=${mdiChevronDown}
class="summary-icon ${classMap({ expanded: this.expanded })}"
></ha-svg-icon>
</div>
<div
class="container ${classMap({ expanded: this.expanded })}"
@transitionend=${this._handleTransitionEnd}
>
<slot></slot>
</div>
`;
}
private _handleTransitionEnd() {
this._container.style.removeProperty("height");
}
private _toggleContainer(): void {
const scrollHeight = this._container.scrollHeight;
this._container.style.height = `${scrollHeight}px`;
if (this.expanded) {
setTimeout(() => {
this._container.style.height = "0px";
}, 0);
}
this.expanded = !this.expanded;
fireEvent(this, "expanded-changed", { expanded: this.expanded });
}
static get styles(): CSSResult {
return css`
:host {
display: block;
}
:host([outlined]) {
box-shadow: none;
border-width: 1px;
border-style: solid;
border-color: var(
--ha-card-border-color,
var(--divider-color, #e0e0e0)
);
border-radius: var(--ha-card-border-radius, 4px);
}
.summary {
display: flex;
padding: 0px 16px;
min-height: 48px;
align-items: center;
cursor: pointer;
overflow: hidden;
}
.summary-icon {
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
margin-left: auto;
}
.summary-icon.expanded {
transform: rotate(180deg);
}
.container {
overflow: hidden;
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
height: 0px;
}
.container.expanded {
height: auto;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-expansion-panel": HaExpansionPanel;
}
// for fire event
interface HASSDomEvents {
"expanded-changed": {
expanded: boolean;
};
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,6 +38,7 @@ class HaHLSPlayer extends LitElement {
@property({ type: Boolean, attribute: "allow-exoplayer" }) @property({ type: Boolean, attribute: "allow-exoplayer" })
public allowExoPlayer = false; public allowExoPlayer = false;
// don't cache this, as we remove it on disconnects
@query("video") private _videoEl!: HTMLVideoElement; @query("video") private _videoEl!: HTMLVideoElement;
@internalProperty() private _attached = false; @internalProperty() private _attached = false;
@ -154,6 +155,9 @@ class HaHLSPlayer extends LitElement {
} }
private _resizeExoPlayer = () => { private _resizeExoPlayer = () => {
if (!this._videoEl) {
return;
}
const rect = this._videoEl.getBoundingClientRect(); const rect = this._videoEl.getBoundingClientRect();
this.hass!.auth.external!.fireMessage({ this.hass!.auth.external!.fireMessage({
type: "exoplayer/resize", type: "exoplayer/resize",

View File

@ -14,8 +14,8 @@ class HaLabeledSlider extends PolymerElement {
} }
.title { .title {
margin-bottom: 16px; margin-bottom: 8px;
color: var(--secondary-text-color); color: var(--primary-text-color);
} }
.slider-container { .slider-container {
@ -43,7 +43,6 @@ class HaLabeledSlider extends PolymerElement {
step="[[step]]" step="[[step]]"
pin="[[pin]]" pin="[[pin]]"
disabled="[[disabled]]" disabled="[[disabled]]"
disabled="[[disabled]]"
value="{{value}}" value="{{value}}"
></ha-slider> ></ha-slider>
</div> </div>

View File

@ -6,9 +6,9 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
@ -62,7 +62,7 @@ class HaMenuButton extends LitElement {
aria-label=${this.hass.localize("ui.sidebar.sidebar_toggle")} aria-label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
@click=${this._toggleMenu} @click=${this._toggleMenu}
> >
<ha-svg-icon path=${mdiMenu}></ha-svg-icon> <ha-svg-icon .path=${mdiMenu}></ha-svg-icon>
</mwc-icon-button> </mwc-icon-button>
${hasNotifications ? html` <div class="dot"></div> ` : ""} ${hasNotifications ? html` <div class="dot"></div> ` : ""}
`; `;
@ -98,8 +98,7 @@ class HaMenuButton extends LitElement {
return; return;
} }
this.style.visibility = this.style.display = newNarrow || this._alwaysVisible ? "initial" : "none";
newNarrow || this._alwaysVisible ? "initial" : "hidden";
if (!newNarrow) { if (!newNarrow) {
this._hasNotifications = false; this._hasNotifications = false;

View File

@ -728,6 +728,7 @@ class HaSidebar extends LitElement {
width: 64px; width: 64px;
} }
:host([expanded]) { :host([expanded]) {
width: 256px;
width: calc(256px + env(safe-area-inset-left)); width: calc(256px + env(safe-area-inset-left));
} }
:host([rtl]) { :host([rtl]) {
@ -735,8 +736,7 @@ class HaSidebar extends LitElement {
border-left: 1px solid var(--divider-color); border-left: 1px solid var(--divider-color);
} }
.menu { .menu {
box-sizing: border-box; height: var(--header-height);
height: 65px;
display: flex; display: flex;
padding: 0 8.5px; padding: 0 8.5px;
border-bottom: 1px solid transparent; border-bottom: 1px solid transparent;
@ -793,7 +793,10 @@ class HaSidebar extends LitElement {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
box-sizing: border-box; box-sizing: border-box;
height: calc(100% - 196px - env(safe-area-inset-bottom)); height: calc(100% - var(--header-height) - 132px);
height: calc(
100% - var(--header-height) - 132px - env(safe-area-inset-bottom)
);
overflow-x: hidden; overflow-x: hidden;
background: none; background: none;
margin-left: env(safe-area-inset-left); margin-left: env(safe-area-inset-left);

View File

@ -21,6 +21,7 @@ class HaSlider extends PaperSliderClass {
.pin > .slider-knob > .slider-knob-inner { .pin > .slider-knob > .slider-knob-inner {
font-size: var(--ha-slider-pin-font-size, 10px); font-size: var(--ha-slider-pin-font-size, 10px);
line-height: normal; line-height: normal;
cursor: pointer;
} }
.disabled.ring > .slider-knob > .slider-knob-inner { .disabled.ring > .slider-knob > .slider-knob-inner {

100
src/components/ha-tabs.ts Normal file
View File

@ -0,0 +1,100 @@
import "@polymer/paper-tabs/paper-tabs";
import type { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";
import type { PaperTabElement } from "@polymer/paper-tabs/paper-tab";
import type { PaperTabsElement } from "@polymer/paper-tabs/paper-tabs";
import { customElement } from "lit-element";
import { Constructor } from "../types";
const PaperTabs = customElements.get("paper-tabs") as Constructor<
PaperTabsElement
>;
let subTemplate: HTMLTemplateElement;
@customElement("ha-tabs")
export class HaTabs extends PaperTabs {
private _firstTabWidth = 0;
private _lastTabWidth = 0;
private _lastLeftHiddenState = false;
static get template(): HTMLTemplateElement {
if (!subTemplate) {
subTemplate = (PaperTabs as any).template.cloneNode(true);
const superStyle = subTemplate.content.querySelector("style");
// Add "noink" attribute for scroll buttons to disable animation.
subTemplate.content
.querySelectorAll("paper-icon-button")
.forEach((arrow: PaperIconButtonElement) => {
arrow.setAttribute("noink", "");
});
superStyle!.appendChild(
document.createTextNode(`
:host {
padding-top: .5px;
}
.not-visible {
display: none;
}
paper-icon-button {
width: 24px;
height: 48px;
padding: 0;
margin: 0;
}
`)
);
}
return subTemplate;
}
// Get first and last tab's width for _affectScroll
public _tabChanged(tab: PaperTabElement, old: PaperTabElement): void {
super._tabChanged(tab, old);
const tabs = this.querySelectorAll("paper-tab:not(.hide-tab)");
if (tabs.length > 0) {
this._firstTabWidth = tabs[0].clientWidth;
this._lastTabWidth = tabs[tabs.length - 1].clientWidth;
}
// Scroll active tab into view if needed.
const selected = this.querySelector(".iron-selected");
if (selected) {
selected.scrollIntoView();
}
}
/**
* Modify _affectScroll so that when the scroll arrows appear
* while scrolling and the tab container shrinks we can counteract
* the jump in tab position so that the scroll still appears smooth.
*/
public _affectScroll(dx: number): void {
if (this._firstTabWidth === 0 || this._lastTabWidth === 0) {
return;
}
this.$.tabsContainer.scrollLeft += dx;
const scrollLeft = this.$.tabsContainer.scrollLeft;
this._leftHidden = scrollLeft - this._firstTabWidth < 0;
this._rightHidden =
scrollLeft + this._lastTabWidth > this._tabContainerScrollSize;
if (this._lastLeftHiddenState !== this._leftHidden) {
this._lastLeftHiddenState = this._leftHidden;
this.$.tabsContainer.scrollLeft += this._leftHidden ? -23 : 23;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-tabs": HaTabs;
}
}

View File

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

View File

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

View File

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

View File

@ -378,6 +378,7 @@ export class HaMediaPlayerBrowse extends LitElement {
: html` : html`
<div class="container"> <div class="container">
${this.hass.localize("ui.components.media-browser.no_items")} ${this.hass.localize("ui.components.media-browser.no_items")}
<br />
${currentItem.media_content_id === ${currentItem.media_content_id ===
"media-source://media_source/local/." "media-source://media_source/local/."
? html`<br />${this.hass.localize( ? html`<br />${this.hass.localize(
@ -398,7 +399,7 @@ export class HaMediaPlayerBrowse extends LitElement {
<br /> <br />
${this.hass.localize( ${this.hass.localize(
"ui.components.media-browser.local_media_files" "ui.components.media-browser.local_media_files"
)}.` )}`
: ""} : ""}
</div> </div>
`} `}

View File

@ -159,7 +159,7 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
if (prevState !== null) { if (prevState !== null) {
dataRow.push([prevLastChanged, endTime, locState, prevState]); dataRow.push([prevLastChanged, endTime, locState, prevState]);
} }
datasets.push({ data: dataRow }); datasets.push({ data: dataRow, entity_id: stateInfo.entity_id });
labels.push(entityDisplay); labels.push(entityDisplay);
}); });
@ -173,12 +173,22 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
return [state, start, end]; 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 = { const chartOptions = {
type: "timeline", type: "timeline",
options: { options: {
tooltips: { tooltips: {
callbacks: { callbacks: {
label: formatTooltipLabel, label: formatTooltipLabel,
beforeBody: formatTooltipBeforeBody,
}, },
}, },
scales: { scales: {

View File

@ -24,10 +24,14 @@ class HaUserPicker extends LitElement {
@property() public label?: string; @property() public label?: string;
@property() public value?: string; @property() public noUserLabel?: string;
@property() public value = "";
@property() public users?: User[]; @property() public users?: User[];
@property({ type: Boolean }) public disabled = false;
private _sortedUsers = memoizeOne((users?: User[]) => { private _sortedUsers = memoizeOne((users?: User[]) => {
if (!users) { if (!users) {
return []; return [];
@ -40,15 +44,19 @@ class HaUserPicker extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<paper-dropdown-menu-light .label=${this.label}> <paper-dropdown-menu-light
.label=${this.label}
.disabled=${this.disabled}
>
<paper-listbox <paper-listbox
slot="dropdown-content" slot="dropdown-content"
.selected=${this._value} .selected=${this.value}
attr-for-selected="data-user-id" attr-for-selected="data-user-id"
@iron-select=${this._userChanged} @iron-select=${this._userChanged}
> >
<paper-icon-item data-user-id=""> <paper-icon-item data-user-id="">
No user ${this.noUserLabel ||
this.hass?.localize("ui.components.user-picker.no_user")}
</paper-icon-item> </paper-icon-item>
${this._sortedUsers(this.users).map( ${this._sortedUsers(this.users).map(
(user) => html` (user) => html`
@ -67,10 +75,6 @@ class HaUserPicker extends LitElement {
`; `;
} }
private get _value() {
return this.value || "";
}
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
if (this.users === undefined) { if (this.users === undefined) {
@ -83,7 +87,7 @@ class HaUserPicker extends LitElement {
private _userChanged(ev) { private _userChanged(ev) {
const newValue = ev.detail.item.dataset.userId; const newValue = ev.detail.item.dataset.userId;
if (newValue !== this._value) { if (newValue !== this.value) {
this.value = ev.detail.value; this.value = ev.detail.value;
setTimeout(() => { setTimeout(() => {
fireEvent(this, "value-changed", { value: newValue }); fireEvent(this, "value-changed", { value: newValue });
@ -111,3 +115,9 @@ class HaUserPicker extends LitElement {
} }
customElements.define("ha-user-picker", HaUserPicker); customElements.define("ha-user-picker", HaUserPicker);
declare global {
interface HTMLElementTagNameMap {
"ha-user-picker": HaUserPicker;
}
}

View File

@ -0,0 +1,169 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import type { PolymerChangedEvent } from "../../polymer-types";
import type { HomeAssistant } from "../../types";
import { fetchUsers, User } from "../../data/user";
import "./ha-user-picker";
import { mdiClose } from "@mdi/js";
import memoizeOne from "memoize-one";
import { guard } from "lit-html/directives/guard";
@customElement("ha-users-picker")
class HaUsersPickerLight extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public value?: string[];
@property({ attribute: "picked-user-label" })
public pickedUserLabel?: string;
@property({ attribute: "pick-user-label" })
public pickUserLabel?: string;
@property({ attribute: false })
public users?: User[];
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
if (this.users === undefined) {
fetchUsers(this.hass!).then((users) => {
this.users = users;
});
}
}
protected render(): TemplateResult {
if (!this.hass || !this.users) {
return html``;
}
const notSelectedUsers = this._notSelectedUsers(this.users, this.value);
return html`
${guard([notSelectedUsers], () =>
this.value?.map(
(user_id, idx) => html`
<div>
<ha-user-picker
.label=${this.pickedUserLabel}
.noUserLabel=${this.hass?.localize(
"ui.components.user-picker.remove_user"
)}
.index=${idx}
.hass=${this.hass}
.value=${user_id}
.users=${this._notSelectedUsersAndSelected(
user_id,
this.users,
notSelectedUsers
)}
@value-changed=${this._userChanged}
></ha-user-picker>
<mwc-icon-button .userId=${user_id} @click=${this._removeUser}>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
</div>
`
)
)}
<ha-user-picker
.noUserLabel=${this.pickUserLabel ||
this.hass?.localize("ui.components.user-picker.add_user")}
.hass=${this.hass}
.users=${notSelectedUsers}
.disabled=${!notSelectedUsers?.length}
@value-changed=${this._addUser}
></ha-user-picker>
`;
}
private _notSelectedUsers = memoizeOne(
(users?: User[], currentUsers?: string[]) =>
currentUsers
? users?.filter(
(user) => !user.system_generated && !currentUsers.includes(user.id)
)
: users?.filter((user) => !user.system_generated)
);
private _notSelectedUsersAndSelected = (
userId: string,
users?: User[],
notSelected?: User[]
) => {
const selectedUser = users?.find((user) => user.id === userId);
if (selectedUser) {
return notSelected ? [...notSelected, selectedUser] : [selectedUser];
}
return notSelected;
};
private get _currentUsers() {
return this.value || [];
}
private async _updateUsers(users) {
this.value = users;
fireEvent(this, "value-changed", {
value: users,
});
}
private _userChanged(event: PolymerChangedEvent<string>) {
event.stopPropagation();
const index = (event.currentTarget as any).index;
const newValue = event.detail.value;
const newUsers = [...this._currentUsers];
if (newValue === "") {
newUsers.splice(index, 1);
} else {
newUsers.splice(index, 1, newValue);
}
this._updateUsers(newUsers);
}
private async _addUser(event: PolymerChangedEvent<string>) {
event.stopPropagation();
const toAdd = event.detail.value;
(event.currentTarget as any).value = "";
if (!toAdd) {
return;
}
const currentUsers = this._currentUsers;
if (currentUsers.includes(toAdd)) {
return;
}
this._updateUsers([...currentUsers, toAdd]);
}
private _removeUser(event) {
const userId = (event.currentTarget as any).userId;
this._updateUsers(this._currentUsers.filter((user) => user !== userId));
}
static get styles(): CSSResult {
return css`
:host {
display: block;
}
div {
display: flex;
align-items: center;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-users-picker": HaUsersPickerLight;
}
}

View File

@ -109,10 +109,17 @@ export interface TemplateTrigger {
value_template: string; value_template: string;
} }
export interface ContextConstraint {
context_id?: string;
parent_id?: string;
user_id?: string | string[];
}
export interface EventTrigger { export interface EventTrigger {
platform: "event"; platform: "event";
event_type: string; event_type: string;
event_data: any; event_data?: any;
context?: ContextConstraint;
} }
export type Trigger = export type Trigger =
@ -217,12 +224,12 @@ export const subscribeTrigger = (
hass: HomeAssistant, hass: HomeAssistant,
onChange: (result: { onChange: (result: {
variables: { variables: {
trigger: {}; trigger: Record<string, unknown>;
}; };
context: Context; context: Context;
}) => void, }) => void,
trigger: Trigger | Trigger[], trigger: Trigger | Trigger[],
variables?: {} variables?: Record<string, unknown>
) => ) =>
hass.connection.subscribeMessage(onChange, { hass.connection.subscribeMessage(onChange, {
type: "subscribe_trigger", type: "subscribe_trigger",
@ -233,7 +240,7 @@ export const subscribeTrigger = (
export const testCondition = ( export const testCondition = (
hass: HomeAssistant, hass: HomeAssistant,
condition: Condition | Condition[], condition: Condition | Condition[],
variables?: {} variables?: Record<string, unknown>
) => ) =>
hass.callWS<{ result: boolean }>({ hass.callWS<{ result: boolean }>({
type: "test_condition", type: "test_condition",

View File

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

View File

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

51
src/data/counter.ts Normal file
View File

@ -0,0 +1,51 @@
import { HomeAssistant } from "../types";
export interface Counter {
id: string;
name: string;
icon?: string;
initial?: number;
restore?: boolean;
minimum?: number;
maximum?: number;
step?: number;
}
export interface CounterMutableParams {
name: string;
icon: string;
initial: number;
restore: boolean;
minimum: number;
maximum: number;
step: number;
}
export const fetchCounter = (hass: HomeAssistant) =>
hass.callWS<Counter[]>({ type: "counter/list" });
export const createCounter = (
hass: HomeAssistant,
values: CounterMutableParams
) =>
hass.callWS<Counter>({
type: "counter/create",
...values,
});
export const updateCounter = (
hass: HomeAssistant,
id: string,
updates: Partial<CounterMutableParams>
) =>
hass.callWS<Counter>({
type: "counter/update",
counter_id: id,
...updates,
});
export const deleteCounter = (hass: HomeAssistant, id: string) =>
hass.callWS({
type: "counter/delete",
counter_id: id,
});

View File

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

View File

@ -2,78 +2,71 @@ import { HomeAssistant } from "../../types";
import { hassioApiResultExtractor, HassioResponse } from "./common"; import { hassioApiResultExtractor, HassioResponse } from "./common";
export interface HassioAddonInfo { export interface HassioAddonInfo {
name: string; advanced: boolean;
slug: string;
description: string;
repository: "core" | "local" | string;
version: string;
state: "none" | "started" | "stopped";
installed: string | undefined;
detached: boolean;
available: boolean; available: boolean;
build: boolean; build: boolean;
advanced: boolean; description: string;
url: string | null; detached: boolean;
icon: boolean; icon: boolean;
installed: boolean;
logo: boolean; logo: boolean;
name: string;
repository: "core" | "local" | string;
slug: string;
stage: "stable" | "experimental" | "deprecated";
state: "started" | "stopped" | null;
update_available: boolean;
url: string | null;
version_latest: string;
version: string;
} }
export interface HassioAddonDetails extends HassioAddonInfo { export interface HassioAddonDetails extends HassioAddonInfo {
name: string;
slug: string;
description: string;
long_description: null | string;
auto_update: boolean;
url: null | string;
detached: boolean;
documentation: boolean;
available: boolean;
arch: "armhf" | "aarch64" | "i386" | "amd64";
machine: any;
homeassistant: string;
version_latest: string;
boot: "auto" | "manual";
build: boolean;
options: object;
network: null | object;
network_description: null | object;
host_network: boolean;
host_pid: boolean;
host_ipc: boolean;
host_dbus: boolean;
privileged: any;
apparmor: "disable" | "default" | "profile"; apparmor: "disable" | "default" | "profile";
devices: string[]; arch: "armhf" | "aarch64" | "i386" | "amd64";
auto_uart: boolean;
icon: boolean;
logo: boolean;
stage: "stable" | "experimental" | "deprecated";
changelog: boolean;
hassio_api: boolean;
hassio_role: "default" | "homeassistant" | "manager" | "admin";
startup: "initialize" | "system" | "services" | "application" | "once";
homeassistant_api: boolean;
auth_api: boolean;
full_access: boolean;
protected: boolean;
rating: "1-6";
stdin: boolean;
webui: null | string;
gpio: boolean;
kernel_modules: boolean;
devicetree: boolean;
docker_api: boolean;
audio: boolean;
audio_input: null | string; audio_input: null | string;
audio_output: null | string; audio_output: null | string;
services_role: string[]; audio: boolean;
auth_api: boolean;
auto_uart: boolean;
auto_update: boolean;
boot: "auto" | "manual";
changelog: boolean;
devices: string[];
devicetree: boolean;
discovery: string[]; discovery: string[];
ip_address: string; docker_api: boolean;
ingress: boolean; documentation: boolean;
ingress_panel: boolean; full_access: boolean;
gpio: boolean;
hassio_api: boolean;
hassio_role: "default" | "homeassistant" | "manager" | "admin";
homeassistant_api: boolean;
homeassistant: string;
host_dbus: boolean;
host_ipc: boolean;
host_network: boolean;
host_pid: boolean;
ingress_entry: null | string; ingress_entry: null | string;
ingress_panel: boolean;
ingress_url: null | string; ingress_url: null | string;
ingress: boolean;
ip_address: string;
kernel_modules: boolean;
long_description: null | string;
machine: any;
network_description: null | Record<string, string>;
network: null | Record<string, number>;
options: Record<string, unknown>;
privileged: any;
protected: boolean;
rating: "1-6";
services_role: string[];
slug: string;
startup: "initialize" | "system" | "services" | "application" | "once";
stdin: boolean;
watchdog: null | boolean; watchdog: null | boolean;
webui: null | string;
} }
export interface HassioAddonsInfo { export interface HassioAddonsInfo {
@ -96,11 +89,11 @@ export interface HassioAddonRepository {
export interface HassioAddonSetOptionParams { export interface HassioAddonSetOptionParams {
audio_input?: string | null; audio_input?: string | null;
audio_output?: string | null; audio_output?: string | null;
options?: object | null; options?: Record<string, unknown> | null;
boot?: "auto" | "manual"; boot?: "auto" | "manual";
auto_update?: boolean; auto_update?: boolean;
ingress_panel?: boolean; ingress_panel?: boolean;
network?: object | null; network?: Record<string, unknown> | null;
watchdog?: boolean; watchdog?: boolean;
} }

View File

@ -22,8 +22,8 @@ export const hassioApiResultExtractor = <T>(response: HassioResponse<T>) =>
export const extractApiErrorMessage = (error: any): string => { export const extractApiErrorMessage = (error: any): string => {
return typeof error === "object" return typeof error === "object"
? typeof error.body === "object" ? typeof error.body === "object"
? error.body.message || "Unknown error, see logs" ? error.body.message || "Unknown error, see supervisor logs"
: error.body || "Unknown error, see logs" : error.body || error.message || "Unknown error, see supervisor logs"
: error; : error;
}; };

36
src/data/hassio/docker.ts Normal file
View File

@ -0,0 +1,36 @@
import { HomeAssistant } from "../../types";
import { hassioApiResultExtractor, HassioResponse } from "./common";
interface HassioDockerRegistries {
[key: string]: { username: string; password?: string };
}
export const fetchHassioDockerRegistries = async (hass: HomeAssistant) => {
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioDockerRegistries>>(
"GET",
"hassio/docker/registries"
)
);
};
export const addHassioDockerRegistry = async (
hass: HomeAssistant,
data: HassioDockerRegistries
) => {
await hass.callApi<HassioResponse<HassioDockerRegistries>>(
"POST",
"hassio/docker/registries",
data
);
};
export const removeHassioDockerRegistry = async (
hass: HomeAssistant,
registry: string
) => {
await hass.callApi<HassioResponse<void>>(
"DELETE",
`hassio/docker/registries/${registry}`
);
};

View File

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

View File

@ -1,14 +1,25 @@
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { hassioApiResultExtractor, HassioResponse } from "./common"; import { hassioApiResultExtractor, HassioResponse } from "./common";
export type HassioHostInfo = any; export type HassioHostInfo = {
chassis: string;
cpe: string;
deployment: string;
disk_free: number;
disk_total: number;
disk_used: number;
features: string[];
hostname: string;
kernel: string;
operating_system: string;
};
export interface HassioHassOSInfo { export interface HassioHassOSInfo {
version: string; board: string;
version_cli: string; boot: string;
update_available: boolean;
version_latest: string; version_latest: string;
version_cli_latest: string; version: string;
board: "ova" | "rpi";
} }
export const fetchHassioHostInfo = async (hass: HomeAssistant) => { export const fetchHassioHostInfo = async (hass: HomeAssistant) => {

View File

@ -0,0 +1,15 @@
import { HomeAssistant } from "../../types";
import { hassioApiResultExtractor, HassioResponse } from "./common";
export interface HassioResolution {
unsupported: string[];
}
export const fetchHassioResolution = async (hass: HomeAssistant) => {
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioResolution>>(
"GET",
"hassio/resolution/info"
)
);
};

View File

@ -1,19 +1,56 @@
import { HomeAssistant, PanelInfo } from "../../types"; import { HomeAssistant, PanelInfo } from "../../types";
import { HassioAddonInfo, HassioAddonRepository } from "./addon";
import { hassioApiResultExtractor, HassioResponse } from "./common"; import { hassioApiResultExtractor, HassioResponse } from "./common";
export type HassioHomeAssistantInfo = any; export type HassioHomeAssistantInfo = {
export type HassioSupervisorInfo = any; arch: string;
audio_input: string | null;
audio_output: string | null;
boot: boolean;
image: string;
ip_address: string;
machine: string;
port: number;
ssl: boolean;
update_available: boolean;
version_latest: string;
version: string;
wait_boot: number;
watchdog: boolean;
};
export type HassioSupervisorInfo = {
addons: HassioAddonInfo[];
addons_repositories: HassioAddonRepository[];
arch: string;
channel: string;
debug: boolean;
debug_block: boolean;
diagnostics: boolean | null;
healthy: boolean;
ip_address: string;
logging: string;
supported: boolean;
timezone: string;
update_available: boolean;
version: string;
version_latest: string;
wait_boot: number;
};
export type HassioInfo = { export type HassioInfo = {
arch: string; arch: string;
channel: string; channel: string;
docker: string; docker: string;
hassos?: string; features: string[];
hassos: null;
homeassistant: string; homeassistant: string;
hostname: string; hostname: string;
logging: string; logging: string;
maching: string; machine: string;
operating_system: string;
supervisor: string; supervisor: string;
supported: boolean;
supported_arch: string[]; supported_arch: string[];
timezone: string; timezone: string;
}; };

View File

@ -85,11 +85,14 @@ export const fetchRecent = (
export const fetchDate = ( export const fetchDate = (
hass: HomeAssistant, hass: HomeAssistant,
startTime: Date, startTime: Date,
endTime: Date endTime: Date,
entityId
): Promise<HassEntity[][]> => { ): Promise<HassEntity[][]> => {
return hass.callApi( return hass.callApi(
"GET", "GET",
`history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}&minimal_response` `history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}&minimal_response${
entityId ? `&filter_entity_id=${entityId}` : ``
}`
); );
}; };

View File

@ -6,6 +6,7 @@ import { HomeAssistant } from "../types";
import { UNAVAILABLE_STATES } from "./entity"; import { UNAVAILABLE_STATES } from "./entity";
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages"; const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
export const CONTINUOUS_DOMAINS = ["proximity", "sensor"];
export interface LogbookEntry { export interface LogbookEntry {
when: string; when: string;

View File

@ -33,7 +33,7 @@ export interface LovelaceResource {
} }
export interface LovelaceResourcesMutableParams { export interface LovelaceResourcesMutableParams {
res_type: "css" | "js" | "module" | "html"; res_type: LovelaceResource["type"];
url: string; url: string;
} }

View File

@ -63,6 +63,16 @@ export interface OZWNetworkStatistics {
retries: number; retries: number;
} }
export interface OZWDeviceConfig {
label: string;
type: string;
value: string | number;
parameter: number;
min: number;
max: number;
help: string;
}
export const nodeQueryStages = [ export const nodeQueryStages = [
"ProtocolInfo", "ProtocolInfo",
"Probe", "Probe",
@ -180,6 +190,17 @@ export const fetchOZWNodeMetadata = (
node_id: node_id, node_id: node_id,
}); });
export const fetchOZWNodeConfig = (
hass: HomeAssistant,
ozw_instance: number,
node_id: number
): Promise<OZWDeviceConfig[]> =>
hass.callWS({
type: "ozw/get_config_parameters",
ozw_instance: ozw_instance,
node_id: node_id,
});
export const refreshNodeInfo = ( export const refreshNodeInfo = (
hass: HomeAssistant, hass: HomeAssistant,
ozw_instance: number, ozw_instance: number,

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