Compare commits

..

7 Commits

Author SHA1 Message Date
Bram Kragten
cf03f103ab Merge pull request #9285 from home-assistant/dev 2021-05-28 12:45:21 +02:00
Bram Kragten
4a8a7c997e Merge pull request #9267 from home-assistant/dev 2021-05-26 17:33:32 +02:00
Bram Kragten
9612bc78fe Merge pull request #9097 from home-assistant/dev 2021-05-04 23:21:05 +02:00
Bram Kragten
2b86137388 Merge pull request #9079 from home-assistant/dev 2021-05-03 16:16:58 +02:00
Paulus Schoutsen
8fdbe447c1 Merge pull request #9060 from home-assistant/dev
20210430.0
2021-04-30 12:43:33 -07:00
Bram Kragten
764ae7e0b6 Merge pull request #9045 from home-assistant/dev 2021-04-29 22:21:03 +02:00
Paulus Schoutsen
6b7e78320d Merge pull request #9024 from home-assistant/dev 2021-04-28 10:47:16 -07:00
217 changed files with 3022 additions and 9042 deletions

View File

@@ -104,6 +104,5 @@
"lit/attribute-value-entities": 0 "lit/attribute-value-entities": 0
}, },
"plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"], "plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"],
"processor": "disable/disable", "processor": "disable/disable"
"ignorePatterns": ["src/resources/lit-virtualizer/*"]
} }

View File

@@ -6,6 +6,7 @@ on:
- published - published
env: env:
WHEELS_TAG: 3.8-alpine3.12
PYTHON_VERSION: 3.8 PYTHON_VERSION: 3.8
NODE_VERSION: 12.1 NODE_VERSION: 12.1
@@ -63,9 +64,6 @@ jobs:
strategy: strategy:
matrix: matrix:
arch: ["aarch64", "armhf", "armv7", "amd64", "i386"] arch: ["aarch64", "armhf", "armv7", "amd64", "i386"]
tag:
- "3.8-alpine3.12"
- "3.9-alpine3.13"
steps: steps:
- name: Download requirements.txt - name: Download requirements.txt
uses: actions/download-artifact@v2 uses: actions/download-artifact@v2
@@ -75,7 +73,7 @@ jobs:
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@master uses: home-assistant/wheels@master
with: with:
tag: ${{ matrix.tag }} tag: ${{ env.WHEELS_TAG }}
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
wheels-host: ${{ secrets.WHEELS_HOST }} wheels-host: ${{ secrets.WHEELS_HOST }}
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}

21
.gitignore vendored
View File

@@ -1,17 +1,10 @@
.DS_Store
.reify-cache
# build
build build
build-translations/* build-translations/*
hass_frontend/*
dist
# yarn
.yarn
yarn-error.log
node_modules/* node_modules/*
npm-debug.log npm-debug.log
.DS_Store
hass_frontend/*
.reify-cache
# Python stuff # Python stuff
*.py[cod] *.py[cod]
@@ -21,8 +14,11 @@ npm-debug.log
# venv stuff # venv stuff
pyvenv.cfg pyvenv.cfg
pip-selfcheck.json pip-selfcheck.json
venv/* venv
.venv .venv
lib
bin
dist
# vscode # vscode
.vscode/* .vscode/*
@@ -35,8 +31,9 @@ src/cast/dev_const.ts
# Secrets # Secrets
.lokalise_token .lokalise_token
yarn-error.log
# asdf #asdf
.tool-versions .tool-versions
# Home Assistant config # Home Assistant config

View File

@@ -52,7 +52,6 @@ module.exports.terserOptions = (latestBuild) => ({
module.exports.babelOptions = ({ latestBuild }) => ({ module.exports.babelOptions = ({ latestBuild }) => ({
babelrc: false, babelrc: false,
compact: false,
presets: [ presets: [
!latestBuild && [ !latestBuild && [
"@babel/preset-env", "@babel/preset-env",
@@ -80,6 +79,12 @@ module.exports.babelOptions = ({ latestBuild }) => ({
].filter(Boolean), ].filter(Boolean),
}); });
// Are already ES5, cause warnings when babelified.
module.exports.babelExclude = () => [
require.resolve("@mdi/js/mdi.js"),
require.resolve("hls.js"),
];
const outputPath = (outputRoot, latestBuild) => const outputPath = (outputRoot, latestBuild) =>
path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5"); path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5");

View File

@@ -1,5 +1,4 @@
// Tasks to run webpack. // Tasks to run webpack.
const fs = require("fs");
const gulp = require("gulp"); const gulp = require("gulp");
const webpack = require("webpack"); const webpack = require("webpack");
const WebpackDevServer = require("webpack-dev-server"); const WebpackDevServer = require("webpack-dev-server");
@@ -19,11 +18,6 @@ const bothBuilds = (createConfigFunc, params) => [
createConfigFunc({ ...params, latestBuild: false }), createConfigFunc({ ...params, latestBuild: false }),
]; ];
const isWsl = fs
.readFileSync("/proc/version", "utf-8")
.toLocaleLowerCase()
.includes("microsoft");
/** /**
* @param {{ * @param {{
* compiler: import("webpack").Compiler, * compiler: import("webpack").Compiler,
@@ -85,7 +79,7 @@ const prodBuild = (conf) =>
gulp.task("webpack-watch-app", () => { gulp.task("webpack-watch-app", () => {
// This command will run forever because we don't close compiler // This command will run forever because we don't close compiler
webpack(createAppConfig({ isProdBuild: false, latestBuild: true })).watch( webpack(createAppConfig({ isProdBuild: false, latestBuild: true })).watch(
{ ignored: /build-translations/, poll: isWsl }, { ignored: /build-translations/ },
doneHandler() doneHandler()
); );
gulp.watch( gulp.watch(
@@ -143,7 +137,7 @@ gulp.task("webpack-watch-hassio", () => {
isProdBuild: false, isProdBuild: false,
latestBuild: true, latestBuild: true,
}) })
).watch({ ignored: /build-translations/, poll: isWsl }, doneHandler()); ).watch({ ignored: /build-translations/ }, doneHandler());
gulp.watch( gulp.watch(
path.join(paths.translations_src, "en.json"), path.join(paths.translations_src, "en.json"),

View File

@@ -57,6 +57,7 @@ const createRollupConfig = ({
babel({ babel({
...bundle.babelOptions({ latestBuild }), ...bundle.babelOptions({ latestBuild }),
extensions, extensions,
exclude: bundle.babelExclude(),
babelHelpers: isWDS ? "inline" : "bundled", babelHelpers: isWDS ? "inline" : "bundled",
}), }),
string({ string({

View File

@@ -47,6 +47,7 @@ const createWebpackConfig = ({
rules: [ rules: [
{ {
test: /\.m?js$|\.ts$/, test: /\.m?js$|\.ts$/,
exclude: bundle.babelExclude(),
use: { use: {
loader: "babel-loader", loader: "babel-loader",
options: bundle.babelOptions({ latestBuild }), options: bundle.babelOptions({ latestBuild }),
@@ -115,9 +116,8 @@ const createWebpackConfig = ({
// We need to change the import of the polyfill for EventTarget, so we replace the polyfill file with our customized one // We need to change the import of the polyfill for EventTarget, so we replace the polyfill file with our customized one
new webpack.NormalModuleReplacementPlugin( new webpack.NormalModuleReplacementPlugin(
new RegExp( new RegExp(
path.resolve( require.resolve(
paths.polymer_dir, "@lit-labs/virtualizer/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js"
"src/resources/lit-virtualizer/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js"
) )
), ),
path.resolve(paths.polymer_dir, "src/resources/EventTarget-ponyfill.js") path.resolve(paths.polymer_dir, "src/resources/EventTarget-ponyfill.js")

View File

@@ -28,11 +28,10 @@ const createConfigEntry = (
title, title,
source: "zeroconf", source: "zeroconf",
state: "loaded", state: "loaded",
connection_class: "local_push",
supports_options: false, supports_options: false,
supports_unload: true, supports_unload: true,
disabled_by: null, disabled_by: null,
pref_disable_new_entities: false,
pref_disable_polling: false,
reason: null, reason: null,
...override, ...override,
}); });
@@ -65,9 +64,6 @@ const configPanelEntry = createConfigEntry("Config Panel", {
const optionsFlowEntry = createConfigEntry("Options Flow", { const optionsFlowEntry = createConfigEntry("Options Flow", {
supports_options: true, supports_options: true,
}); });
const disabledPollingEntry = createConfigEntry("Disabled Polling", {
pref_disable_polling: true,
});
const setupErrorEntry = createConfigEntry("Setup Error", { const setupErrorEntry = createConfigEntry("Setup Error", {
state: "setup_error", state: "setup_error",
}); });
@@ -140,7 +136,6 @@ const configEntries: Array<{
{ items: [loadedEntry] }, { items: [loadedEntry] },
{ items: [configPanelEntry] }, { items: [configPanelEntry] },
{ items: [optionsFlowEntry] }, { items: [optionsFlowEntry] },
{ items: [disabledPollingEntry] },
{ items: [nameAsDomainEntry] }, { items: [nameAsDomainEntry] },
{ items: [longNameEntry] }, { items: [longNameEntry] },
{ items: [longNonBreakingNameEntry] }, { items: [longNonBreakingNameEntry] },

View File

@@ -3,7 +3,6 @@ import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js"; import { mdiDotsVertical } from "@mdi/js";
import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea"; import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea";
import { DEFAULT_SCHEMA, Type } from "js-yaml";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -12,7 +11,7 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, state, query } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button"; import "../../../../src/components/buttons/ha-progress-button";
@@ -28,7 +27,6 @@ import {
HassioAddonDetails, HassioAddonDetails,
HassioAddonSetOptionParams, HassioAddonSetOptionParams,
setHassioAddonOption, setHassioAddonOption,
validateHassioAddonOption,
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { Supervisor } from "../../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../../src/data/supervisor/supervisor";
@@ -40,13 +38,6 @@ import { hassioStyle } from "../../resources/hassio-style";
const SUPPORTED_UI_TYPES = ["string", "select", "boolean", "integer", "float"]; const SUPPORTED_UI_TYPES = ["string", "select", "boolean", "integer", "float"];
const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
new Type("!secret", {
kind: "scalar",
construct: (data) => `!secret ${data}`,
}),
]);
@customElement("hassio-addon-config") @customElement("hassio-addon-config")
class HassioAddonConfig extends LitElement { class HassioAddonConfig extends LitElement {
@property({ attribute: false }) public addon!: HassioAddonDetails; @property({ attribute: false }) public addon!: HassioAddonDetails;
@@ -134,7 +125,6 @@ class HassioAddonConfig extends LitElement {
></ha-form>` ></ha-form>`
: html` <ha-yaml-editor : html` <ha-yaml-editor
@value-changed=${this._configChanged} @value-changed=${this._configChanged}
.schema=${ADDON_YAML_SCHEMA}
></ha-yaml-editor>`} ></ha-yaml-editor>`}
${this._error ? html` <div class="errors">${this._error}</div> ` : ""} ${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
${!this._yamlMode || ${!this._yamlMode ||
@@ -279,14 +269,6 @@ class HassioAddonConfig extends LitElement {
this._error = undefined; this._error = undefined;
try { try {
const validation = await validateHassioAddonOption(
this.hass,
this.addon.slug,
this._editor?.value
);
if (!validation.valid) {
throw Error(validation.message);
}
await setHassioAddonOption(this.hass, this.addon.slug, { await setHassioAddonOption(this.hass, this.addon.slug, {
options: this._yamlMode ? this._editor?.value : this._options, options: this._yamlMode ? this._editor?.value : this._options,
}); });

View File

@@ -977,7 +977,6 @@ class HassioAddonInfo extends LitElement {
showDialogSupervisorUpdate(this, { showDialogSupervisorUpdate(this, {
supervisor: this.supervisor, supervisor: this.supervisor,
name: this.addon.name, name: this.addon.name,
slug: this.addon.slug,
version: this.addon.version_latest, version: this.addon.version_latest,
snapshotParams: { snapshotParams: {
name: `addon_${this.addon.slug}_${this.addon.version}`, name: `addon_${this.addon.slug}_${this.addon.version}`,

View File

@@ -29,6 +29,7 @@ class SupervisorFormfieldLabel extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host { :host {
cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
} }

View File

@@ -5,7 +5,6 @@ import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version"; import { atLeastVersion } from "../../../src/common/config/version";
import { formatDate } from "../../../src/common/datetime/format_date"; import { formatDate } from "../../../src/common/datetime/format_date";
import { formatDateTime } from "../../../src/common/datetime/format_date_time"; import { formatDateTime } from "../../../src/common/datetime/format_date_time";
import { LocalizeFunc } from "../../../src/common/translations/localize";
import "../../../src/components/ha-checkbox"; import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-formfield"; import "../../../src/components/ha-formfield";
import "../../../src/components/ha-radio"; import "../../../src/components/ha-radio";
@@ -45,9 +44,6 @@ const _computeFolders = (folders): CheckboxItem[] => {
if (folders.includes("share")) { if (folders.includes("share")) {
list.push({ slug: "share", name: "Share", checked: false }); list.push({ slug: "share", name: "Share", checked: false });
} }
if (folders.includes("media")) {
list.push({ slug: "media", name: "Media", checked: false });
}
if (folders.includes("addons/local")) { if (folders.includes("addons/local")) {
list.push({ slug: "addons/local", name: "Local add-ons", checked: false }); list.push({ slug: "addons/local", name: "Local add-ons", checked: false });
} }
@@ -68,8 +64,6 @@ const _computeAddons = (addons): AddonCheckboxItem[] =>
export class SupervisorSnapshotContent extends LitElement { export class SupervisorSnapshotContent extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public localize?: LocalizeFunc;
@property({ attribute: false }) public supervisor?: Supervisor; @property({ attribute: false }) public supervisor?: Supervisor;
@property({ attribute: false }) public snapshot?: HassioSnapshotDetail; @property({ attribute: false }) public snapshot?: HassioSnapshotDetail;
@@ -84,14 +78,10 @@ export class SupervisorSnapshotContent extends LitElement {
@property({ type: Boolean }) public snapshotHasPassword = false; @property({ type: Boolean }) public snapshotHasPassword = false;
@property({ type: Boolean }) public onboarding = false;
@property() public snapshotName = ""; @property() public snapshotName = "";
@property() public snapshotPassword = ""; @property() public snapshotPassword = "";
@property() public confirmSnapshotPassword = "";
public willUpdate(changedProps) { public willUpdate(changedProps) {
super.willUpdate(changedProps); super.willUpdate(changedProps);
if (!this.hasUpdated) { if (!this.hasUpdated) {
@@ -111,12 +101,8 @@ export class SupervisorSnapshotContent extends LitElement {
} }
} }
private _localize = (string: string) =>
this.supervisor?.localize(`snapshot.${string}`) ||
this.localize!(`ui.panel.page-onboarding.restore.${string}`);
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.onboarding && !this.supervisor) { if (!this.supervisor) {
return html``; return html``;
} }
const foldersSection = const foldersSection =
@@ -128,16 +114,14 @@ export class SupervisorSnapshotContent extends LitElement {
${this.snapshot ${this.snapshot
? html`<div class="details"> ? html`<div class="details">
${this.snapshot.type === "full" ${this.snapshot.type === "full"
? this._localize("full_snapshot") ? this.supervisor.localize("snapshot.full_snapshot")
: this._localize("partial_snapshot")} : this.supervisor.localize("snapshot.partial_snapshot")}
(${Math.ceil(this.snapshot.size * 10) / 10 + " MB"})<br /> (${Math.ceil(this.snapshot.size * 10) / 10 + " MB"})<br />
${this.hass ${formatDateTime(new Date(this.snapshot.date), this.hass.locale)}
? formatDateTime(new Date(this.snapshot.date), this.hass.locale)
: this.snapshot.date}
</div>` </div>`
: html`<paper-input : html`<paper-input
name="snapshotName" name="snapshotName"
.label=${this.supervisor?.localize("snapshot.name") || "Name"} .label=${this.supervisor.localize("snapshot.name")}
.value=${this.snapshotName} .value=${this.snapshotName}
@value-changed=${this._handleTextValueChanged} @value-changed=${this._handleTextValueChanged}
> >
@@ -145,11 +129,13 @@ export class SupervisorSnapshotContent extends LitElement {
${!this.snapshot || this.snapshot.type === "full" ${!this.snapshot || this.snapshot.type === "full"
? html`<div class="sub-header"> ? html`<div class="sub-header">
${!this.snapshot ${!this.snapshot
? this._localize("type") ? this.supervisor.localize("snapshot.type")
: this._localize("select_type")} : this.supervisor.localize("snapshot.select_type")}
</div> </div>
<div class="snapshot-types"> <div class="snapshot-types">
<ha-formfield .label=${this._localize("full_snapshot")}> <ha-formfield
.label=${this.supervisor.localize("snapshot.full_snapshot")}
>
<ha-radio <ha-radio
@change=${this._handleRadioValueChanged} @change=${this._handleRadioValueChanged}
value="full" value="full"
@@ -158,7 +144,9 @@ export class SupervisorSnapshotContent extends LitElement {
> >
</ha-radio> </ha-radio>
</ha-formfield> </ha-formfield>
<ha-formfield .label=${this._localize("partial_snapshot")}> <ha-formfield
.label=${this.supervisor!.localize("snapshot.partial_snapshot")}
>
<ha-radio <ha-radio
@change=${this._handleRadioValueChanged} @change=${this._handleRadioValueChanged}
value="partial" value="partial"
@@ -169,9 +157,9 @@ export class SupervisorSnapshotContent extends LitElement {
</ha-formfield> </ha-formfield>
</div>` </div>`
: ""} : ""}
${this.snapshotType === "partial" ${this.snapshot && this.snapshotType === "partial"
? html`<div class="partial-picker"> ? html`
${this.snapshot && this.snapshot.homeassistant ${this.snapshot.homeassistant
? html` ? html`
<ha-formfield <ha-formfield
.label=${html`<supervisor-formfield-label .label=${html`<supervisor-formfield-label
@@ -191,11 +179,15 @@ export class SupervisorSnapshotContent extends LitElement {
</ha-formfield> </ha-formfield>
` `
: ""} : ""}
`
: ""}
${this.snapshotType === "partial"
? html`
${foldersSection?.templates.length ${foldersSection?.templates.length
? html` ? html`
<ha-formfield <ha-formfield
.label=${html`<supervisor-formfield-label .label=${html`<supervisor-formfield-label
.label=${this._localize("folders")} .label=${this.supervisor.localize("snapshot.folders")}
.iconPath=${mdiFolder} .iconPath=${mdiFolder}
> >
</supervisor-formfield-label>`} </supervisor-formfield-label>`}
@@ -215,7 +207,7 @@ export class SupervisorSnapshotContent extends LitElement {
? html` ? html`
<ha-formfield <ha-formfield
.label=${html`<supervisor-formfield-label .label=${html`<supervisor-formfield-label
.label=${this._localize("addons")} .label=${this.supervisor.localize("snapshot.addons")}
.iconPath=${mdiPuzzle} .iconPath=${mdiPuzzle}
> >
</supervisor-formfield-label>`} </supervisor-formfield-label>`}
@@ -231,44 +223,29 @@ export class SupervisorSnapshotContent extends LitElement {
<div class="section-content">${addonsSection.templates}</div> <div class="section-content">${addonsSection.templates}</div>
` `
: ""} : ""}
</div> ` `
: ""}
${this.snapshotType === "partial" &&
(!this.snapshot || this.snapshotHasPassword)
? html`<hr />`
: ""} : ""}
${!this.snapshot ${!this.snapshot
? html`<ha-formfield ? html`<ha-formfield
class="password" .label=${this.supervisor.localize("snapshot.password_protection")}
.label=${this._localize("password_protection")}
> >
<ha-checkbox <ha-checkbox
.checked=${this.snapshotHasPassword} .checked=${this.snapshotHasPassword}
@change=${this._toggleHasPassword} @change=${this._toggleHasPassword}
> >
</ha-checkbox> </ha-checkbox
</ha-formfield>` ></ha-formfield>`
: ""} : ""}
${this.snapshotHasPassword ${this.snapshotHasPassword
? html` ? html`
<paper-input <paper-input
.label=${this._localize("password")} .label=${this.supervisor.localize("snapshot.password")}
type="password" type="password"
name="snapshotPassword" name="snapshotPassword"
.value=${this.snapshotPassword} .value=${this.snapshotPassword}
@value-changed=${this._handleTextValueChanged} @value-changed=${this._handleTextValueChanged}
> >
</paper-input> </paper-input>
${!this.snapshot
? html` <paper-input
.label=${this.supervisor?.localize("confirm_password")}
type="password"
name="confirmSnapshotPassword"
.value=${this.confirmSnapshotPassword}
@value-changed=${this._handleTextValueChanged}
>
</paper-input>`
: ""}
` `
: ""} : ""}
`; `;
@@ -276,24 +253,21 @@ export class SupervisorSnapshotContent extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
.partial-picker ha-formfield { ha-checkbox {
--mdc-checkbox-touch-target-size: 16px;
display: block; display: block;
margin: 4px 12px 8px 0;
} }
.partial-picker ha-checkbox { ha-formfield {
--mdc-checkbox-touch-target-size: 32px; display: contents;
}
.partial-picker {
display: block;
margin: 0px -6px;
} }
supervisor-formfield-label { supervisor-formfield-label {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
} }
hr { paper-input[type="password"] {
border-color: var(--divider-color); display: block;
border-bottom: none; margin: 4px 0 4px 16px;
margin: 16px 0;
} }
.details { .details {
color: var(--secondary-text-color); color: var(--secondary-text-color);
@@ -301,15 +275,13 @@ export class SupervisorSnapshotContent extends LitElement {
.section-content { .section-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-left: 30px; margin-left: 16px;
} }
ha-formfield.password { .security {
display: block; margin-top: 16px;
margin: 0 -14px -16px;
} }
.snapshot-types { .snapshot-types {
display: flex; display: flex;
margin-left: -13px;
} }
.sub-header { .sub-header {
margin-top: 8px; margin-top: 8px;
@@ -328,9 +300,6 @@ export class SupervisorSnapshotContent extends LitElement {
if (this.snapshotHasPassword) { if (this.snapshotHasPassword) {
data.password = this.snapshotPassword; data.password = this.snapshotPassword;
if (!this.snapshot) {
data.confirm_password = this.confirmSnapshotPassword;
}
} }
if (this.snapshotType === "full") { if (this.snapshotType === "full") {
@@ -362,7 +331,7 @@ export class SupervisorSnapshotContent extends LitElement {
const addons = const addons =
section === "addons" section === "addons"
? new Map( ? new Map(
this.supervisor?.addon.addons.map((item) => [item.slug, item]) this.supervisor!.addon.addons.map((item) => [item.slug, item])
) )
: undefined; : undefined;
let checkedItems = 0; let checkedItems = 0;
@@ -372,7 +341,6 @@ export class SupervisorSnapshotContent extends LitElement {
.label=${item.name} .label=${item.name}
.iconPath=${section === "addons" ? mdiPuzzle : mdiFolder} .iconPath=${section === "addons" ? mdiPuzzle : mdiFolder}
.imageUrl=${section === "addons" && .imageUrl=${section === "addons" &&
!this.onboarding &&
atLeastVersion(this.hass.config.version, 0, 105) && atLeastVersion(this.hass.config.version, 0, 105) &&
addons?.get(item.slug)?.icon addons?.get(item.slug)?.icon
? `/api/hassio/addons/${item.slug}/icon` ? `/api/hassio/addons/${item.slug}/icon`

View File

@@ -161,7 +161,6 @@ export class HassioUpdate extends LitElement {
showDialogSupervisorUpdate(this, { showDialogSupervisorUpdate(this, {
supervisor: this.supervisor, supervisor: this.supervisor,
name: "Home Assistant Core", name: "Home Assistant Core",
slug: "core",
version: this.supervisor.core.version_latest, version: this.supervisor.core.version_latest,
snapshotParams: { snapshotParams: {
name: `core_${this.supervisor.core.version}`, name: `core_${this.supervisor.core.version}`,

View File

@@ -1,194 +0,0 @@
import { mdiClose } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/common/search/search-input";
import { compare } from "../../../../src/common/string/compare";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
import { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
import { dump } from "../../../../src/resources/js-yaml-dump";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware";
const _filterDevices = memoizeOne(
(showAdvanced: boolean, hardware: HassioHardwareInfo, filter: string) =>
hardware.devices
.filter(
(device) =>
(showAdvanced ||
["tty", "gpio", "input"].includes(device.subsystem)) &&
(device.by_id?.toLowerCase().includes(filter) ||
device.name.toLowerCase().includes(filter) ||
device.dev_path.toLocaleLowerCase().includes(filter) ||
JSON.stringify(device.attributes)
.toLocaleLowerCase()
.includes(filter))
)
.sort((a, b) => compare(a.name, b.name))
);
@customElement("dialog-hassio-hardware")
class HassioHardwareDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _dialogParams?: HassioHardwareDialogParams;
@state() private _filter?: string;
public showDialog(params: HassioHardwareDialogParams) {
this._dialogParams = params;
}
public closeDialog() {
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._dialogParams) {
return html``;
}
const devices = _filterDevices(
this.hass.userData?.showAdvanced || false,
this._dialogParams.hardware,
(this._filter || "").toLowerCase()
);
return html`
<ha-dialog
open
scrimClickAction
hideActions
@closed=${this.closeDialog}
.heading=${true}
>
<div class="header" slot="heading">
<h2>
${this._dialogParams.supervisor.localize("dialog.hardware.title")}
</h2>
<mwc-icon-button dialogAction="close">
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
<search-input
autofocus
no-label-float
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
.label=${this._dialogParams.supervisor.localize(
"dialog.hardware.search"
)}
>
</search-input>
</div>
${devices.map(
(device) =>
html`<ha-expansion-panel
.header=${device.name}
.secondary=${device.by_id || undefined}
outlined
>
<div class="device-property">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.subsystem"
)}:
</span>
<span>${device.subsystem}</span>
</div>
<div class="device-property">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.device_path"
)}:
</span>
<code>${device.dev_path}</code>
</div>
${device.by_id
? html` <div class="device-property">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.id"
)}:
</span>
<code>${device.by_id}</code>
</div>`
: ""}
<div class="attributes">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.attributes"
)}:
</span>
<pre>${dump(device.attributes, { indent: 2 })}</pre>
</div>
</ha-expansion-panel>`
)}
</ha-dialog>
`;
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
mwc-icon-button {
position: absolute;
right: 16px;
top: 10px;
text-decoration: none;
color: var(--primary-text-color);
}
h2 {
margin: 18px 42px 0 18px;
color: var(--primary-text-color);
}
ha-expansion-panel {
margin: 4px 0;
}
pre,
code {
background-color: var(--markdown-code-background-color, none);
border-radius: 3px;
}
pre {
padding: 16px;
overflow: auto;
line-height: 1.45;
font-family: var(--code-font-family, monospace);
}
code {
font-size: 85%;
padding: 0.2em 0.4em;
}
search-input {
margin: 0 16px;
display: block;
}
.device-property {
display: flex;
justify-content: space-between;
}
.attributes {
margin-top: 12px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-hardware": HassioHardwareDialog;
}
}

View File

@@ -1,19 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioHardwareDialogParams {
supervisor: Supervisor;
hardware: HassioHardwareInfo;
}
export const showHassioHardwareDialog = (
element: HTMLElement,
dialogParams: HassioHardwareDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-hardware",
dialogImport: () => import("./dialog-hassio-hardware"),
dialogParams,
});
};

View File

@@ -244,6 +244,9 @@ class HassioRegistriesDialog extends LitElement {
mwc-list-item span[slot="secondary"] { mwc-list-item span[slot="secondary"] {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
ha-paper-dropdown-menu {
display: block;
}
`, `,
]; ];
} }

View File

@@ -150,6 +150,9 @@ class HassioRepositoriesDialog extends LitElement {
mwc-button { mwc-button {
margin-left: 8px; margin-left: 8px;
} }
ha-paper-dropdown-menu {
display: block;
}
ha-circular-progress { ha-circular-progress {
display: block; display: block;
margin: 32px; margin: 32px;

View File

@@ -95,25 +95,16 @@ class HassioCreateSnapshotDialog extends LitElement {
this._creatingSnapshot = true; this._creatingSnapshot = true;
this._error = ""; this._error = "";
if (snapshotDetails.password && !snapshotDetails.password.length) { if (
this._snapshotContent.snapshotHasPassword &&
!this._snapshotContent.snapshotPassword.length
) {
this._error = this._dialogParams!.supervisor.localize( this._error = this._dialogParams!.supervisor.localize(
"snapshot.enter_password" "snapshot.enter_password"
); );
this._creatingSnapshot = false; this._creatingSnapshot = false;
return; return;
} }
if (
snapshotDetails.password &&
snapshotDetails.password !== snapshotDetails.confirm_password
) {
this._error = this._dialogParams!.supervisor.localize(
"snapshot.passwords_not_matching"
);
this._creatingSnapshot = false;
return;
}
delete snapshotDetails.confirm_password;
try { try {
if (this._snapshotContent.snapshotType === "full") { if (this._snapshotContent.snapshotType === "full") {

View File

@@ -1,13 +1,13 @@
import { ActionDetail } from "@material/mwc-list"; import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiClose, mdiDotsVertical } from "@mdi/js"; import { mdiDotsVertical } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import { slugify } from "../../../../src/common/string/slugify"; import { slugify } from "../../../../src/common/string/slugify";
import "../../../../src/components/buttons/ha-progress-button"; import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-button-menu"; import "../../../../src/components/ha-button-menu";
import "../../../../src/components/ha-header-bar"; import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
import { getSignedPath } from "../../../../src/data/auth"; import { getSignedPath } from "../../../../src/data/auth";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
@@ -22,7 +22,6 @@ import {
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager"; import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import { fileDownload } from "../../../../src/util/file_download";
import "../../components/supervisor-snapshot-content"; import "../../components/supervisor-snapshot-content";
import type { SupervisorSnapshotContent } from "../../components/supervisor-snapshot-content"; import type { SupervisorSnapshotContent } from "../../components/supervisor-snapshot-content";
import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot"; import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot";
@@ -67,24 +66,14 @@ class HassioSnapshotDialog
open open
scrimClickAction scrimClickAction
@closed=${this.closeDialog} @closed=${this.closeDialog}
.heading=${true} .heading=${createCloseHeading(this.hass, this._computeName)}
> >
<div slot="heading">
<ha-header-bar>
<span slot="title">${this._snapshot.name}</span>
<mwc-icon-button slot="actionItems" dialogAction="cancel">
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
</ha-header-bar>
</div>
${this._restoringSnapshot ${this._restoringSnapshot
? html` <ha-circular-progress active></ha-circular-progress>` ? html` <ha-circular-progress active></ha-circular-progress>`
: html`<supervisor-snapshot-content : html`<supervisor-snapshot-content
.hass=${this.hass} .hass=${this.hass}
.supervisor=${this._dialogParams.supervisor} .supervisor=${this._dialogParams.supervisor}
.snapshot=${this._snapshot} .snapshot=${this._snapshot}
.onboarding=${this._dialogParams.onboarding || false}
.localize=${this._dialogParams.localize}
> >
</supervisor-snapshot-content>`} </supervisor-snapshot-content>`}
${this._error ? html`<p class="error">Error: ${this._error}</p>` : ""} ${this._error ? html`<p class="error">Error: ${this._error}</p>` : ""}
@@ -97,20 +86,18 @@ class HassioSnapshotDialog
Restore Restore
</mwc-button> </mwc-button>
${!this._dialogParams.onboarding <ha-button-menu
? html`<ha-button-menu fixed
fixed slot="primaryAction"
slot="primaryAction" @action=${this._handleMenuAction}
@action=${this._handleMenuAction} @closed=${(ev: Event) => ev.stopPropagation()}
@closed=${(ev: Event) => ev.stopPropagation()} >
> <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>Download Snapshot</mwc-list-item>
<mwc-list-item>Download Snapshot</mwc-list-item> <mwc-list-item class="error">Delete Snapshot</mwc-list-item>
<mwc-list-item class="error">Delete Snapshot</mwc-list-item> </ha-button-menu>
</ha-button-menu>`
: ""}
</ha-dialog> </ha-dialog>
`; `;
} }
@@ -127,12 +114,6 @@ class HassioSnapshotDialog
display: block; display: block;
text-align: center; text-align: center;
} }
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
display: block;
}
`, `,
]; ];
} }
@@ -307,11 +288,12 @@ class HassioSnapshotDialog
} }
} }
fileDownload( const a = document.createElement("a");
this, a.href = signedPath.path;
signedPath.path, a.download = `home_assistant_snapshot_${slugify(this._computeName)}.tar`;
`home_assistant_snapshot_${slugify(this._computeName)}.tar` this.shadowRoot!.appendChild(a);
); a.click();
this.shadowRoot!.removeChild(a);
} }
private get _computeName() { private get _computeName() {

View File

@@ -1,5 +1,4 @@
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import { LocalizeFunc } from "../../../../src/common/translations/localize";
import { Supervisor } from "../../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioSnapshotDialogParams { export interface HassioSnapshotDialogParams {
@@ -7,7 +6,6 @@ export interface HassioSnapshotDialogParams {
onDelete?: () => void; onDelete?: () => void;
onboarding?: boolean; onboarding?: boolean;
supervisor?: Supervisor; supervisor?: Supervisor;
localize?: LocalizeFunc;
} }
export const showHassioSnapshotDialog = ( export const showHassioSnapshotDialog = (

View File

@@ -2,32 +2,19 @@ import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-checkbox";
import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-settings-row"; import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch";
import { import {
extractApiErrorMessage, extractApiErrorMessage,
ignoreSupervisorError, ignoreSupervisorError,
} from "../../../../src/data/hassio/common"; } from "../../../../src/data/hassio/common";
import {
SupervisorFrontendPrefrences,
fetchSupervisorFrontendPreferences,
saveSupervisorFrontendPreferences,
} from "../../../../src/data/supervisor/supervisor";
import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot"; import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update"; import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update";
import memoizeOne from "memoize-one";
const snapshot_before_update = memoizeOne(
(slug: string, frontendPrefrences: SupervisorFrontendPrefrences) =>
slug in frontendPrefrences.snapshot_before_update
? frontendPrefrences.snapshot_before_update[slug]
: true
);
@customElement("dialog-supervisor-update") @customElement("dialog-supervisor-update")
class DialogSupervisorUpdate extends LitElement { class DialogSupervisorUpdate extends LitElement {
@@ -35,12 +22,12 @@ class DialogSupervisorUpdate extends LitElement {
@state() private _opened = false; @state() private _opened = false;
@state() private _createSnapshot = true;
@state() private _action: "snapshot" | "update" | null = null; @state() private _action: "snapshot" | "update" | null = null;
@state() private _error?: string; @state() private _error?: string;
@state() private _frontendPrefrences?: SupervisorFrontendPrefrences;
@state() @state()
private _dialogParams?: SupervisorDialogSupervisorUpdateParams; private _dialogParams?: SupervisorDialogSupervisorUpdateParams;
@@ -49,17 +36,14 @@ class DialogSupervisorUpdate extends LitElement {
): Promise<void> { ): Promise<void> {
this._opened = true; this._opened = true;
this._dialogParams = params; this._dialogParams = params;
this._frontendPrefrences = await fetchSupervisorFrontendPreferences(
this.hass
);
await this.updateComplete; await this.updateComplete;
} }
public closeDialog(): void { public closeDialog(): void {
this._action = null; this._action = null;
this._createSnapshot = true;
this._error = undefined; this._error = undefined;
this._dialogParams = undefined; this._dialogParams = undefined;
this._frontendPrefrences = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
@@ -72,7 +56,7 @@ class DialogSupervisorUpdate extends LitElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._dialogParams || !this._frontendPrefrences) { if (!this._dialogParams) {
return html``; return html``;
} }
return html` return html`
@@ -98,16 +82,6 @@ class DialogSupervisorUpdate extends LitElement {
</div> </div>
<ha-settings-row> <ha-settings-row>
<ha-checkbox
.checked=${snapshot_before_update(
this._dialogParams.slug,
this._frontendPrefrences
)}
haptic
@click=${this._toggleSnapshot}
slot="prefix"
>
</ha-checkbox>
<span slot="heading"> <span slot="heading">
${this._dialogParams.supervisor.localize( ${this._dialogParams.supervisor.localize(
"dialog.update.snapshot" "dialog.update.snapshot"
@@ -120,6 +94,12 @@ class DialogSupervisorUpdate extends LitElement {
this._dialogParams.name this._dialogParams.name
)} )}
</span> </span>
<ha-switch
.checked=${this._createSnapshot}
haptic
@click=${this._toggleSnapshot}
>
</ha-switch>
</ha-settings-row> </ha-settings-row>
<mwc-button @click=${this.closeDialog} slot="secondaryAction"> <mwc-button @click=${this.closeDialog} slot="secondaryAction">
${this._dialogParams.supervisor.localize("common.cancel")} ${this._dialogParams.supervisor.localize("common.cancel")}
@@ -153,27 +133,12 @@ class DialogSupervisorUpdate extends LitElement {
`; `;
} }
private async _toggleSnapshot(): Promise<void> { private _toggleSnapshot() {
this._frontendPrefrences!.snapshot_before_update[ this._createSnapshot = !this._createSnapshot;
this._dialogParams!.slug
] = !snapshot_before_update(
this._dialogParams!.slug,
this._frontendPrefrences!
);
await saveSupervisorFrontendPreferences(
this.hass,
this._frontendPrefrences!
);
} }
private async _update() { private async _update() {
if ( if (this._createSnapshot) {
snapshot_before_update(
this._dialogParams!.slug,
this._frontendPrefrences!
)
) {
this._action = "snapshot"; this._action = "snapshot";
try { try {
await createHassioPartialSnapshot( await createHassioPartialSnapshot(

View File

@@ -5,7 +5,6 @@ export interface SupervisorDialogSupervisorUpdateParams {
supervisor: Supervisor; supervisor: Supervisor;
name: string; name: string;
version: string; version: string;
slug: string;
snapshotParams: any; snapshotParams: any;
updateHandler: () => Promise<void>; updateHandler: () => Promise<void>;
} }

View File

@@ -97,23 +97,16 @@ class HassioIngressView extends LitElement {
title: requestedAddon, title: requestedAddon,
}); });
await nextRender(); await nextRender();
navigate("/hassio/store", { replace: true }); history.back();
return; return;
} }
if (!addonInfo.version) { if (!addonInfo.ingress) {
await showAlertDialog(this, {
text: this.supervisor.localize("my.error_addon_not_installed"),
title: addonInfo.name,
});
await nextRender();
navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true });
} else if (!addonInfo.ingress) {
await showAlertDialog(this, { await showAlertDialog(this, {
text: this.supervisor.localize("my.error_addon_no_ingress"), text: this.supervisor.localize("my.error_addon_no_ingress"),
title: addonInfo.name, title: addonInfo.name,
}); });
await nextRender(); await nextRender();
navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true }); history.back();
} else { } else {
navigate(`/hassio/ingress/${addonInfo.slug}`, { replace: true }); navigate(`/hassio/ingress/${addonInfo.slug}`, { replace: true });
} }

View File

@@ -1,17 +1,15 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { ActionDetail } from "@material/mwc-list"; import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js"; import { mdiDotsVertical, mdiPlus } from "@mdi/js";
import { import {
css,
CSSResultGroup, CSSResultGroup,
html, html,
LitElement, LitElement,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version"; import { atLeastVersion } from "../../../src/common/config/version";
import relativeTime from "../../../src/common/datetime/relative_time"; import relativeTime from "../../../src/common/datetime/relative_time";
@@ -19,25 +17,18 @@ import { HASSDomEvent } from "../../../src/common/dom/fire_event";
import { import {
DataTableColumnContainer, DataTableColumnContainer,
RowClickedEvent, RowClickedEvent,
SelectionChangedEvent,
} from "../../../src/components/data-table/ha-data-table"; } from "../../../src/components/data-table/ha-data-table";
import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-fab"; import "../../../src/components/ha-fab";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { import {
fetchHassioSnapshots, fetchHassioSnapshots,
friendlyFolderName, friendlyFolderName,
HassioSnapshot, HassioSnapshot,
reloadHassioSnapshots, reloadHassioSnapshots,
removeSnapshot,
} from "../../../src/data/hassio/snapshot"; } from "../../../src/data/hassio/snapshot";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-tabs-subpage-data-table"; import "../../../src/layouts/hass-tabs-subpage-data-table";
import type { HaTabsSubpageDataTable } from "../../../src/layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types"; import { HomeAssistant, Route } from "../../../src/types";
import { showHassioCreateSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-create-snapshot"; import { showHassioCreateSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-create-snapshot";
@@ -58,15 +49,10 @@ export class HassioSnapshots extends LitElement {
@property({ type: Boolean }) public isWide!: boolean; @property({ type: Boolean }) public isWide!: boolean;
@state() private _selectedSnapshots: string[] = []; private _firstUpdatedCalled = false;
@state() private _snapshots?: HassioSnapshot[] = []; @state() private _snapshots?: HassioSnapshot[] = [];
@query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable;
private _firstUpdatedCalled = false;
public connectedCallback(): void { public connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
if (this.hass && this._firstUpdatedCalled) { if (this.hass && this._firstUpdatedCalled) {
@@ -167,9 +153,7 @@ export class HassioSnapshots extends LitElement {
.data=${this._snapshotData(this._snapshots || [])} .data=${this._snapshotData(this._snapshots || [])}
id="slug" id="slug"
@row-click=${this._handleRowClicked} @row-click=${this._handleRowClicked}
@selection-changed=${this._handleSelectionChanged}
clickable clickable
selectable
hasFab hasFab
main-page main-page
supervisor supervisor
@@ -192,45 +176,6 @@ export class HassioSnapshots extends LitElement {
: ""} : ""}
</ha-button-menu> </ha-button-menu>
${this._selectedSnapshots.length
? html`<div
class=${classMap({
"header-toolbar": this.narrow,
"table-header": !this.narrow,
})}
slot="header"
>
<p class="selected-txt">
${this.supervisor.localize("snapshot.selected", {
number: this._selectedSnapshots.length,
})}
</p>
<div class="header-btns">
${!this.narrow
? html`
<mwc-button
@click=${this._deleteSelected}
class="warning"
>
${this.supervisor.localize("snapshot.delete_selected")}
</mwc-button>
`
: html`
<mwc-icon-button
id="delete-btn"
class="warning"
@click=${this._deleteSelected}
>
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
</mwc-icon-button>
<paper-tooltip animation-delay="0" for="delete-btn">
${this.supervisor.localize("snapshot.delete_selected")}
</paper-tooltip>
`}
</div>
</div> `
: ""}
<ha-fab <ha-fab
slot="fab" slot="fab"
@click=${this._createSnapshot} @click=${this._createSnapshot}
@@ -254,12 +199,6 @@ export class HassioSnapshots extends LitElement {
} }
} }
private _handleSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selectedSnapshots = ev.detail.value;
}
private _showUploadSnapshotDialog() { private _showUploadSnapshotDialog() {
showSnapshotUploadDialog(this, { showSnapshotUploadDialog(this, {
showSnapshot: (slug: string) => showSnapshot: (slug: string) =>
@@ -277,35 +216,6 @@ export class HassioSnapshots extends LitElement {
this._snapshots = await fetchHassioSnapshots(this.hass); this._snapshots = await fetchHassioSnapshots(this.hass);
} }
private async _deleteSelected() {
const confirm = await showConfirmationDialog(this, {
title: this.supervisor.localize("snapshot.delete_snapshot_title"),
text: this.supervisor.localize("snapshot.delete_snapshot_text", {
number: this._selectedSnapshots.length,
}),
confirmText: this.supervisor.localize("snapshot.delete_snapshot_confirm"),
});
if (!confirm) {
return;
}
try {
await Promise.all(
this._selectedSnapshots.map((slug) => removeSnapshot(this.hass, slug))
);
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize("snapshot.failed_to_delete"),
text: extractApiErrorMessage(err),
});
return;
}
await reloadHassioSnapshots(this.hass);
this._snapshots = await fetchHassioSnapshots(this.hass);
this._dataTable.clearSelection();
}
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) { private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const slug = ev.detail.id; const slug = ev.detail.id;
showHassioSnapshotDialog(this, { showHassioSnapshotDialog(this, {
@@ -334,45 +244,7 @@ export class HassioSnapshots extends LitElement {
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [haStyle, hassioStyle];
haStyle,
hassioStyle,
css`
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 58px;
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
}
.header-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--secondary-text-color);
position: relative;
top: -4px;
}
.selected-txt {
font-weight: bold;
padding-left: 16px;
color: var(--primary-text-color);
}
.table-header .selected-txt {
margin-top: 20px;
}
.header-toolbar .selected-txt {
font-size: 16px;
}
.header-toolbar .header-btns {
margin-right: -12px;
}
.header-btns > mwc-button,
.header-btns > mwc-icon-button {
margin: 8px;
}
`,
];
} }
} }

View File

@@ -164,7 +164,6 @@ class HassioCoreInfo extends LitElement {
showDialogSupervisorUpdate(this, { showDialogSupervisorUpdate(this, {
supervisor: this.supervisor, supervisor: this.supervisor,
name: "Home Assistant Core", name: "Home Assistant Core",
slug: "core",
version: this.supervisor.core.version_latest, version: this.supervisor.core.version_latest,
snapshotParams: { snapshotParams: {
name: `core_${this.supervisor.core.version}`, name: `core_${this.supervisor.core.version}`,

View File

@@ -2,6 +2,7 @@ import "@material/mwc-button";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js"; import { mdiDotsVertical } from "@mdi/js";
import { dump } from "js-yaml";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@@ -40,8 +41,8 @@ import {
roundWithOneDecimal, roundWithOneDecimal,
} from "../../../src/util/calculate"; } from "../../../src/util/calculate";
import "../components/supervisor-metric"; import "../components/supervisor-metric";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
import { showNetworkDialog } from "../dialogs/network/show-dialog-network"; import { showNetworkDialog } from "../dialogs/network/show-dialog-network";
import { showHassioHardwareDialog } from "../dialogs/hardware/show-dialog-hassio-hardware";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-host-info") @customElement("hassio-host-info")
@@ -228,19 +229,20 @@ class HassioHostInfo extends LitElement {
} }
private async _showHardware(): Promise<void> { private async _showHardware(): Promise<void> {
let hardware;
try { try {
hardware = await fetchHassioHardwareInfo(this.hass); const content = await fetchHassioHardwareInfo(this.hass);
showHassioMarkdownDialog(this, {
title: this.supervisor.localize("system.host.hardware"),
content: `<pre>${dump(content, { indent: 2 })}</pre>`,
});
} catch (err) { } catch (err) {
await showAlertDialog(this, { showAlertDialog(this, {
title: this.supervisor.localize( title: this.supervisor.localize(
"system.host.failed_to_get_hardware_list" "system.host.failed_to_get_hardware_list"
), ),
text: extractApiErrorMessage(err), text: extractApiErrorMessage(err),
}); });
return;
} }
showHassioHardwareDialog(this, { supervisor: this.supervisor, hardware });
} }
private async _hostReboot(ev: CustomEvent): Promise<void> { private async _hostReboot(ev: CustomEvent): Promise<void> {

View File

@@ -1,4 +1,5 @@
module.exports = { module.exports = {
"*.ts": () => "tsc -p tsconfig.json",
"*.{js,ts}": "eslint --fix", "*.{js,ts}": "eslint --fix",
"!(/translations)*.{js,ts,json,css,md,html}": "prettier --write", "!(/translations)*.{js,ts,json,css,md,html}": "prettier --write",
}; };

View File

@@ -66,9 +66,12 @@
"@polymer/iron-autogrow-textarea": "^3.0.1", "@polymer/iron-autogrow-textarea": "^3.0.1",
"@polymer/iron-flex-layout": "^3.0.1", "@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1", "@polymer/iron-icon": "^3.0.1",
"@polymer/iron-image": "^3.0.1",
"@polymer/iron-input": "^3.0.1", "@polymer/iron-input": "^3.0.1",
"@polymer/iron-label": "^3.0.1",
"@polymer/iron-overlay-behavior": "^3.0.2", "@polymer/iron-overlay-behavior": "^3.0.2",
"@polymer/iron-resizable-behavior": "^3.0.1", "@polymer/iron-resizable-behavior": "^3.0.1",
"@polymer/paper-card": "^3.0.1",
"@polymer/paper-checkbox": "^3.1.0", "@polymer/paper-checkbox": "^3.1.0",
"@polymer/paper-dialog": "^3.0.1", "@polymer/paper-dialog": "^3.0.1",
"@polymer/paper-dialog-behavior": "^3.0.1", "@polymer/paper-dialog-behavior": "^3.0.1",
@@ -106,7 +109,7 @@
"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",
"hls.js": "^1.0.5", "hls.js": "^1.0.4",
"home-assistant-js-websocket": "^5.10.0", "home-assistant-js-websocket": "^5.10.0",
"idb-keyval": "^5.0.5", "idb-keyval": "^5.0.5",
"intl-messageformat": "^9.6.16", "intl-messageformat": "^9.6.16",

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup( setup(
name="home-assistant-frontend", name="home-assistant-frontend",
version="20210603.0", version="20210528.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

@@ -6,7 +6,8 @@ export type LeafletDrawModuleType = typeof import("leaflet-draw");
export const setupLeafletMap = async ( export const setupLeafletMap = async (
mapElement: HTMLElement, mapElement: HTMLElement,
darkMode?: boolean darkMode?: boolean,
draw = false
): Promise<[Map, LeafletModuleType, TileLayer]> => { ): Promise<[Map, LeafletModuleType, TileLayer]> => {
if (!mapElement.parentNode) { if (!mapElement.parentNode) {
throw new Error("Cannot setup Leaflet map on disconnected element"); throw new Error("Cannot setup Leaflet map on disconnected element");
@@ -16,6 +17,10 @@ export const setupLeafletMap = async (
.default as LeafletModuleType; .default as LeafletModuleType;
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/"; Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
if (draw) {
await import("leaflet-draw");
}
const map = Leaflet.map(mapElement); const map = Leaflet.map(mapElement);
const style = document.createElement("link"); const style = document.createElement("link");
style.setAttribute("href", "/static/images/leaflet/leaflet.css"); style.setAttribute("href", "/static/images/leaflet/leaflet.css");

View File

@@ -89,6 +89,8 @@ export const domainIcon = (
} }
// eslint-disable-next-line // eslint-disable-next-line
console.warn(`Unable to find icon for domain ${domain}`); console.warn(
"Unable to find icon for domain " + domain + " (" + stateObj + ")"
);
return DEFAULT_DOMAIN_ICON; return DEFAULT_DOMAIN_ICON;
}; };

View File

@@ -1,16 +1,9 @@
import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMagnify } from "@mdi/js"; import { mdiClose, mdiMagnify } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { import { customElement, property } from "lit/decorators";
css, import { classMap } from "lit/directives/class-map";
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../components/ha-svg-icon"; import "../../components/ha-svg-icon";
import { fireEvent } from "../dom/fire_event"; import { fireEvent } from "../dom/fire_event";
@@ -34,11 +27,18 @@ class SearchInput extends LitElement {
this.shadowRoot!.querySelector("paper-input")!.focus(); this.shadowRoot!.querySelector("paper-input")!.focus();
} }
@query("paper-input", true) private _input!: PaperInputElement;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<style>
.no-underline:not(.focused) {
--paper-input-container-underline: {
display: none;
height: 0;
}
}
</style>
<paper-input <paper-input
class=${classMap({ "no-underline": this.noUnderline })}
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.label=${this.label || "Search"} .label=${this.label || "Search"}
.value=${this.filter} .value=${this.filter}
@@ -62,17 +62,6 @@ class SearchInput extends LitElement {
`; `;
} }
protected updated(changedProps: PropertyValues) {
if (
changedProps.has("noUnderline") &&
(this.noUnderline || changedProps.get("noUnderline") !== undefined)
) {
(this._input.inputElement!.parentElement!.shadowRoot!.querySelector(
"div.unfocused-line"
) as HTMLElement).style.display = this.noUnderline ? "none" : "block";
}
}
private async _filterChanged(value: string) { private async _filterChanged(value: string) {
fireEvent(this, "value-changed", { value: String(value) }); fireEvent(this, "value-changed", { value: String(value) });
} }

View File

@@ -1,2 +0,0 @@
export const escapeRegExp = (text: string): string =>
text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");

View File

@@ -1,6 +0,0 @@
import { refine, string } from "superstruct";
const isCustomType = (value: string) => value.startsWith("custom:");
export const customType = () =>
refine(string(), "custom element type", isCustomType);

View File

@@ -1,6 +1,11 @@
import { refine, string } from "superstruct"; import { refine, string } from "superstruct";
const isEntityId = (value: string): boolean => value.includes("."); const isEntityId = (value: string): boolean => {
if (!value.includes(".")) {
return false;
}
return true;
};
export const entityId = () => export const entityId = () =>
refine(string(), "entity ID (domain.entity)", isEntityId); refine(string(), "entity ID (domain.entity)", isEntityId);

View File

@@ -1,5 +1,10 @@
import { refine, string } from "superstruct"; import { refine, string } from "superstruct";
const isIcon = (value: string) => value.includes(":"); const isIcon = (value: string) => {
if (!value.includes(":")) {
return false;
}
return true;
};
export const icon = () => refine(string(), "icon (mdi:icon-name)", isIcon); export const icon = () => refine(string(), "icon (mdi:icon-name)", isIcon);

View File

@@ -86,15 +86,11 @@ export const computeLocalize = async (
| undefined; | undefined;
if (!translatedMessage) { if (!translatedMessage) {
try { translatedMessage = new IntlMessageFormat(
translatedMessage = new IntlMessageFormat( translatedValue,
translatedValue, language,
language, formats
formats );
);
} catch (err) {
return "Translation error: " + err.message;
}
cache._localizationCache[messageKey] = translatedMessage; cache._localizationCache[messageKey] = translatedMessage;
} }

View File

@@ -4,25 +4,29 @@
// be triggered. The function will be called after it stops being called for // be triggered. The function will be called after it stops being called for
// 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
export const debounce = <T extends any[]>( export const debounce = <T extends (...args) => unknown>(
func: (...args: T) => void, func: T,
wait: number, wait,
immediate = false immediate = false
) => { ): T => {
let timeout: number | undefined; let timeout;
return (...args: T): void => { // @ts-ignore
return function (...args) {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-this-alias
const context = this;
const later = () => { const later = () => {
timeout = undefined; timeout = null;
if (!immediate) { if (!immediate) {
func(...args); func.apply(context, args);
} }
}; };
const callNow = immediate && !timeout; const callNow = immediate && !timeout;
clearTimeout(timeout); clearTimeout(timeout);
timeout = window.setTimeout(later, wait); timeout = setTimeout(later, wait);
if (callNow) { if (callNow) {
func(...args); func.apply(context, args);
} }
}; };
}; };

View File

@@ -1,4 +1,4 @@
import { Layout1d, scroll } from "../../resources/lit-virtualizer"; import { Layout1d, scroll } from "@lit-labs/virtualizer";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import { import {
css, css,
@@ -246,7 +246,7 @@ export class HaDataTable extends LitElement {
aria-rowcount=${this._filteredData.length + 1} aria-rowcount=${this._filteredData.length + 1}
style=${styleMap({ style=${styleMap({
height: this.autoHeight height: this.autoHeight
? `${(this._filteredData.length || 1) * 53 + 53}px` ? `${(this._filteredData.length || 1) * 53 + 57}px`
: `calc(100% - ${this._headerHeight}px)`, : `calc(100% - ${this._headerHeight}px)`,
})} })}
> >
@@ -340,10 +340,11 @@ export class HaDataTable extends LitElement {
${scroll({ ${scroll({
items: this._items, items: this._items,
layout: Layout1d, layout: Layout1d,
// @ts-expect-error
renderItem: (row: DataTableRowData, index) => { renderItem: (row: DataTableRowData, index) => {
// not sure how this happens... // not sure how this happens...
if (!row) { if (!row) {
return html``; return "";
} }
if (row.append) { if (row.append) {
return html` return html`
@@ -919,11 +920,13 @@ export class HaDataTable extends LitElement {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
.scroller { .scroller {
display: flex;
position: relative;
contain: strict;
height: calc(100% - 57px); height: calc(100% - 57px);
} }
.mdc-data-table__table:not(.auto-height) .scroller {
.mdc-data-table__table.auto-height .scroller { overflow: auto;
overflow-y: hidden !important;
} }
.grows { .grows {
flex-grow: 1; flex-grow: 1;

View File

@@ -4,6 +4,7 @@ import {
fetchDeviceActions, fetchDeviceActions,
localizeDeviceAutomationAction, localizeDeviceAutomationAction,
} from "../../data/device_automation"; } from "../../data/device_automation";
import "../ha-paper-dropdown-menu";
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker"; import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-action-picker") @customElement("ha-device-action-picker")

View File

@@ -4,6 +4,7 @@ import {
fetchDeviceConditions, fetchDeviceConditions,
localizeDeviceAutomationCondition, localizeDeviceAutomationCondition,
} from "../../data/device_automation"; } from "../../data/device_automation";
import "../ha-paper-dropdown-menu";
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker"; import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-condition-picker") @customElement("ha-device-condition-picker")

View File

@@ -4,6 +4,7 @@ import {
fetchDeviceTriggers, fetchDeviceTriggers,
localizeDeviceAutomationTrigger, localizeDeviceAutomationTrigger,
} from "../../data/device_automation"; } from "../../data/device_automation";
import "../ha-paper-dropdown-menu";
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker"; import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-trigger-picker") @customElement("ha-device-trigger-picker")

View File

@@ -54,17 +54,17 @@ class HaAttributes extends LitElement {
</div> </div>
` `
)} )}
${this.stateObj.attributes.attribution
? html`
<div class="attribution">
${this.stateObj.attributes.attribution}
</div>
`
: ""}
` `
: ""} : ""}
</div> </div>
</ha-expansion-panel> </ha-expansion-panel>
${this.stateObj.attributes.attribution
? html`
<div class="attribution">
${this.stateObj.attributes.attribution}
</div>
`
: ""}
`; `;
} }
@@ -91,7 +91,6 @@ class HaAttributes extends LitElement {
.attribution { .attribution {
color: var(--secondary-text-color); color: var(--secondary-text-color);
text-align: center; text-align: center;
margin-top: 16px;
} }
pre { pre {
font-family: inherit; font-family: inherit;

View File

@@ -14,17 +14,12 @@ class HaExpansionPanel extends LitElement {
@property() header?: string; @property() header?: string;
@property() secondary?: string;
@query(".container") private _container!: HTMLDivElement; @query(".container") private _container!: HTMLDivElement;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div class="summary" @click=${this._toggleContainer}> <div class="summary" @click=${this._toggleContainer}>
<slot class="header" name="header"> <slot name="header">${this.header}</slot>
${this.header}
<slot class="secondary" name="secondary">${this.secondary}</slot>
</slot>
<ha-svg-icon <ha-svg-icon
.path=${mdiChevronDown} .path=${mdiChevronDown}
class="summary-icon ${classMap({ expanded: this.expanded })}" class="summary-icon ${classMap({ expanded: this.expanded })}"
@@ -111,16 +106,6 @@ class HaExpansionPanel extends LitElement {
.container.expanded { .container.expanded {
height: auto; height: auto;
} }
.header {
display: block;
}
.secondary {
display: block;
color: var(--secondary-text-color);
font-size: 12px;
}
`; `;
} }
} }

View File

@@ -7,14 +7,14 @@ import { afterNextRender } from "../common/util/render-status";
import { FrontendLocaleData } from "../data/translation"; import { FrontendLocaleData } from "../data/translation";
import { getValueInPercentage, normalize } from "../util/calculate"; import { getValueInPercentage, normalize } from "../util/calculate";
// Workaround for https://github.com/home-assistant/frontend/issues/6467
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
const getAngle = (value: number, min: number, max: number) => { const getAngle = (value: number, min: number, max: number) => {
const percentage = getValueInPercentage(normalize(value, min, max), min, max); const percentage = getValueInPercentage(normalize(value, min, max), min, max);
return (percentage * 180) / 100; return (percentage * 180) / 100;
}; };
// Workaround for https://github.com/home-assistant/frontend/issues/6467
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
@customElement("ha-gauge") @customElement("ha-gauge")
export class Gauge extends LitElement { export class Gauge extends LitElement {
@property({ type: Number }) public min = 0; @property({ type: Number }) public min = 0;

View File

@@ -13,11 +13,6 @@ import { nextRender } from "../common/util/render-status";
import { getExternalConfig } from "../external_app/external_config"; import { getExternalConfig } from "../external_app/external_config";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
type HlsLite = Omit<
HlsType,
"subtitleTrackController" | "audioTrackController" | "emeController"
>;
@customElement("ha-hls-player") @customElement("ha-hls-player")
class HaHLSPlayer extends LitElement { class HaHLSPlayer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -44,7 +39,7 @@ class HaHLSPlayer extends LitElement {
@state() private _attached = false; @state() private _attached = false;
private _hlsPolyfillInstance?: HlsLite; private _hlsPolyfillInstance?: HlsType;
private _useExoPlayer = false; private _useExoPlayer = false;
@@ -108,8 +103,7 @@ class HaHLSPlayer extends LitElement {
const useExoPlayerPromise = this._getUseExoPlayer(); const useExoPlayerPromise = this._getUseExoPlayer();
const masterPlaylistPromise = fetch(this.url); const masterPlaylistPromise = fetch(this.url);
const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min.js")) const Hls = (await import("hls.js")).default;
.default;
let hlsSupported = Hls.isSupported(); let hlsSupported = Hls.isSupported();
if (!hlsSupported) { if (!hlsSupported) {
@@ -188,7 +182,7 @@ class HaHLSPlayer extends LitElement {
url: string url: string
) { ) {
const hls = new Hls({ const hls = new Hls({
backBufferLength: 60, liveBackBufferLength: 60,
fragLoadingTimeOut: 30000, fragLoadingTimeOut: 30000,
manifestLoadingTimeOut: 30000, manifestLoadingTimeOut: 30000,
levelLoadingTimeOut: 30000, levelLoadingTimeOut: 30000,

View File

@@ -1,12 +1,5 @@
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import { import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
css,
CSSResultGroup,
html,
nothing,
LitElement,
TemplateResult,
} from "lit";
import { customElement, state, property } from "lit/decorators"; import { customElement, state, property } from "lit/decorators";
import { import {
Adapter, Adapter,
@@ -24,19 +17,18 @@ import "./ha-icon";
const format_addresses = ( const format_addresses = (
addresses: IPv6ConfiguredAddress[] | IPv4ConfiguredAddress[] addresses: IPv6ConfiguredAddress[] | IPv4ConfiguredAddress[]
): TemplateResult => ): TemplateResult[] =>
html`${addresses.map((address, i) => [ addresses.map(
html`<span>${address.address}/${address.network_prefix}</span>`, (address) => html`<span>${address.address}/${address.network_prefix}</span>`
i < addresses.length - 1 ? ", " : nothing, );
])}`;
const format_auto_detected_interfaces = ( const format_auto_detected_interfaces = (
adapters: Adapter[] adapters: Adapter[]
): Array<TemplateResult | string> => ): Array<TemplateResult | string> =>
adapters.map((adapter) => adapters.map((adapter) =>
adapter.auto adapter.auto
? html`${adapter.name} ? html`${adapter.name} (${format_addresses(adapter.ipv4)}
(${format_addresses([...adapter.ipv4, ...adapter.ipv6])})` ${format_addresses(adapter.ipv6)} )`
: "" : ""
); );
@@ -96,7 +88,8 @@ export class HaNetwork extends LitElement {
: ""} : ""}
</span> </span>
<span slot="description"> <span slot="description">
${format_addresses([...adapter.ipv4, ...adapter.ipv6])} ${format_addresses(adapter.ipv4)}
${format_addresses(adapter.ipv6)}
</span> </span>
</ha-settings-row>` </ha-settings-row>`
) )

View File

@@ -1,4 +1,4 @@
import { DEFAULT_SCHEMA, dump, load, Schema } from "js-yaml"; import { dump, load } from "js-yaml";
import { html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
@@ -20,8 +20,6 @@ const isEmpty = (obj: Record<string, unknown>): boolean => {
export class HaYamlEditor extends LitElement { export class HaYamlEditor extends LitElement {
@property() public value?: any; @property() public value?: any;
@property({ attribute: false }) public yamlSchema: Schema = DEFAULT_SCHEMA;
@property() public defaultValue?: any; @property() public defaultValue?: any;
@property() public isValid = true; @property() public isValid = true;
@@ -32,10 +30,7 @@ export class HaYamlEditor extends LitElement {
public setValue(value): void { public setValue(value): void {
try { try {
this._yaml = this._yaml = value && !isEmpty(value) ? dump(value) : "";
value && !isEmpty(value)
? dump(value, { schema: this.yamlSchema })
: "";
} catch (err) { } catch (err) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(err, value); console.error(err, value);
@@ -72,7 +67,7 @@ export class HaYamlEditor extends LitElement {
if (this._yaml) { if (this._yaml) {
try { try {
parsed = load(this._yaml, { schema: this.yamlSchema }); parsed = load(this._yaml);
} catch (err) { } catch (err) {
// Invalid YAML // Invalid YAML
isValid = false; isValid = false;

View File

@@ -1,69 +0,0 @@
import { LitElement, html, css } from "lit";
import { property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
class HaEntityMarker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "entity-id" }) public entityId?: string;
@property({ attribute: "entity-name" }) public entityName?: string;
@property({ attribute: "entity-picture" }) public entityPicture?: string;
@property({ attribute: "entity-color" }) public entityColor?: string;
protected render() {
return html`
<div
class="marker"
style=${styleMap({ "border-color": this.entityColor })}
@click=${this._badgeTap}
>
${this.entityPicture
? html`<div
class="entity-picture"
style=${styleMap({
"background-image": `url(${this.entityPicture})`,
})}
></div>`
: this.entityName}
</div>
`;
}
private _badgeTap(ev: Event) {
ev.stopPropagation();
if (this.entityId) {
fireEvent(this, "hass-more-info", { entityId: this.entityId });
}
}
static get styles() {
return css`
.marker {
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
overflow: hidden;
width: 48px;
height: 48px;
font-size: var(--ha-marker-font-size, 1.5em);
border-radius: 50%;
border: 1px solid var(--ha-marker-color, var(--primary-color));
color: var(--primary-text-color);
background-color: var(--card-background-color);
}
.entity-picture {
background-size: cover;
height: 100%;
width: 100%;
}
`;
}
}
customElements.define("ha-entity-marker", HaEntityMarker);

View File

@@ -0,0 +1,299 @@
import {
Circle,
DivIcon,
DragEndEvent,
LatLng,
LeafletMouseEvent,
Map,
Marker,
TileLayer,
} from "leaflet";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import {
LeafletModuleType,
replaceTileLayer,
setupLeafletMap,
} from "../../common/dom/setup-leaflet-map";
import { nextRender } from "../../common/util/render-status";
import { defaultRadiusColor } from "../../data/zone";
import { HomeAssistant } from "../../types";
@customElement("ha-location-editor")
class LocationEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Array }) public location?: [number, number];
@property({ type: Number }) public radius?: number;
@property() public radiusColor?: string;
@property() public icon?: string;
@property({ type: Boolean }) public darkMode?: boolean;
public fitZoom = 16;
private _iconEl?: DivIcon;
private _ignoreFitToMap?: [number, number];
// eslint-disable-next-line
private Leaflet?: LeafletModuleType;
private _leafletMap?: Map;
private _tileLayer?: TileLayer;
private _locationMarker?: Marker | Circle;
public fitMap(): void {
if (!this._leafletMap || !this.location) {
return;
}
if (this._locationMarker && "getBounds" in this._locationMarker) {
this._leafletMap.fitBounds(this._locationMarker.getBounds());
} else {
this._leafletMap.setView(this.location, this.fitZoom);
}
this._ignoreFitToMap = this.location;
}
protected render(): TemplateResult {
return html` <div id="map"></div> `;
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._initMap();
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
// Still loading.
if (!this.Leaflet) {
return;
}
if (changedProps.has("location")) {
this._updateMarker();
if (
this.location &&
(!this._ignoreFitToMap ||
this._ignoreFitToMap[0] !== this.location[0] ||
this._ignoreFitToMap[1] !== this.location[1])
) {
this.fitMap();
}
}
if (changedProps.has("radius")) {
this._updateRadius();
}
if (changedProps.has("radiusColor")) {
this._updateRadiusColor();
}
if (changedProps.has("icon")) {
this._updateIcon();
}
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
return;
}
if (!this._leafletMap || !this._tileLayer) {
return;
}
this._tileLayer = replaceTileLayer(
this.Leaflet,
this._leafletMap,
this._tileLayer,
this.hass.themes.darkMode
);
}
}
private get _mapEl(): HTMLDivElement {
return this.shadowRoot!.querySelector("div")!;
}
private async _initMap(): Promise<void> {
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
this._mapEl,
this.darkMode ?? this.hass.themes.darkMode,
Boolean(this.radius)
);
this._leafletMap.addEventListener(
"click",
// @ts-ignore
(ev: LeafletMouseEvent) => this._locationUpdated(ev.latlng)
);
this._updateIcon();
this._updateMarker();
this.fitMap();
this._leafletMap.invalidateSize();
}
private _locationUpdated(latlng: LatLng) {
let longitude = latlng.lng;
if (Math.abs(longitude) > 180.0) {
// Normalize longitude if map provides values beyond -180 to +180 degrees.
longitude = (((longitude % 360.0) + 540.0) % 360.0) - 180.0;
}
this.location = this._ignoreFitToMap = [latlng.lat, longitude];
fireEvent(this, "change", undefined, { bubbles: false });
}
private _radiusUpdated() {
this._ignoreFitToMap = this.location;
this.radius = (this._locationMarker as Circle).getRadius();
fireEvent(this, "change", undefined, { bubbles: false });
}
private _updateIcon() {
if (!this.icon) {
this._iconEl = undefined;
return;
}
// create icon
let iconHTML = "";
const el = document.createElement("ha-icon");
el.setAttribute("icon", this.icon);
iconHTML = el.outerHTML;
this._iconEl = this.Leaflet!.divIcon({
html: iconHTML,
iconSize: [24, 24],
className: "light leaflet-edit-move",
});
this._setIcon();
}
private _setIcon() {
if (!this._locationMarker || !this._iconEl) {
return;
}
if (!this.radius) {
(this._locationMarker as Marker).setIcon(this._iconEl);
return;
}
// @ts-ignore
const moveMarker = this._locationMarker.editing._moveMarker;
moveMarker.setIcon(this._iconEl);
}
private _setupEdit() {
// @ts-ignore
this._locationMarker.editing.enable();
// @ts-ignore
const moveMarker = this._locationMarker.editing._moveMarker;
// @ts-ignore
const resizeMarker = this._locationMarker.editing._resizeMarkers[0];
this._setIcon();
moveMarker.addEventListener(
"dragend",
// @ts-ignore
(ev: DragEndEvent) => this._locationUpdated(ev.target.getLatLng())
);
resizeMarker.addEventListener(
"dragend",
// @ts-ignore
(ev: DragEndEvent) => this._radiusUpdated(ev)
);
}
private async _updateMarker(): Promise<void> {
if (!this.location) {
if (this._locationMarker) {
this._locationMarker.remove();
this._locationMarker = undefined;
}
return;
}
if (this._locationMarker) {
this._locationMarker.setLatLng(this.location);
if (this.radius) {
// @ts-ignore
this._locationMarker.editing.disable();
await nextRender();
this._setupEdit();
}
return;
}
if (!this.radius) {
this._locationMarker = this.Leaflet!.marker(this.location, {
draggable: true,
});
this._setIcon();
this._locationMarker.addEventListener(
"dragend",
// @ts-ignore
(ev: DragEndEvent) => this._locationUpdated(ev.target.getLatLng())
);
this._leafletMap!.addLayer(this._locationMarker);
} else {
this._locationMarker = this.Leaflet!.circle(this.location, {
color: this.radiusColor || defaultRadiusColor,
radius: this.radius,
});
this._leafletMap!.addLayer(this._locationMarker);
this._setupEdit();
}
}
private _updateRadius(): void {
if (!this._locationMarker || !this.radius) {
return;
}
(this._locationMarker as Circle).setRadius(this.radius);
}
private _updateRadiusColor(): void {
if (!this._locationMarker || !this.radius) {
return;
}
(this._locationMarker as Circle).setStyle({ color: this.radiusColor });
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
height: 300px;
}
#map {
height: 100%;
background: inherit;
}
.leaflet-edit-move {
border-radius: 50%;
cursor: move !important;
}
.leaflet-edit-resize {
border-radius: 50%;
cursor: nesw-resize !important;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-location-editor": LocationEditor;
}
}

View File

@@ -3,8 +3,10 @@ import {
DivIcon, DivIcon,
DragEndEvent, DragEndEvent,
LatLng, LatLng,
Map,
Marker, Marker,
MarkerOptions, MarkerOptions,
TileLayer,
} from "leaflet"; } from "leaflet";
import { import {
css, css,
@@ -14,13 +16,15 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map"; import {
import type { HomeAssistant } from "../../types"; LeafletModuleType,
import "./ha-map"; replaceTileLayer,
import type { HaMap } from "./ha-map"; setupLeafletMap,
} from "../../common/dom/setup-leaflet-map";
import { defaultRadiusColor } from "../../data/zone";
import { HomeAssistant } from "../../types";
declare global { declare global {
// for fire event // for fire event
@@ -47,40 +51,38 @@ export interface MarkerLocation {
export class HaLocationsEditor extends LitElement { export class HaLocationsEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public locations?: MarkerLocation[]; @property() public locations?: MarkerLocation[];
@property({ type: Boolean }) public autoFit = false; public fitZoom = 16;
@property({ type: Number }) public zoom = 16;
@property({ type: Boolean }) public darkMode?: boolean;
@state() private _locationMarkers?: Record<string, Marker | Circle>;
@state() private _circles: Record<string, Circle> = {};
@query("ha-map", true) private map!: HaMap;
// eslint-disable-next-line
private Leaflet?: LeafletModuleType; private Leaflet?: LeafletModuleType;
constructor() { // eslint-disable-next-line
super(); private _leafletMap?: Map;
import("leaflet").then((module) => { private _tileLayer?: TileLayer;
import("leaflet-draw").then(() => {
this.Leaflet = module.default as LeafletModuleType; private _locationMarkers?: { [key: string]: Marker | Circle };
this._updateMarkers();
this.updateComplete.then(() => this.fitMap()); private _circles: Record<string, Circle> = {};
});
});
}
public fitMap(): void { public fitMap(): void {
this.map.fitMap(); if (
!this._leafletMap ||
!this._locationMarkers ||
!Object.keys(this._locationMarkers).length
) {
return;
}
const bounds = this.Leaflet!.latLngBounds(
Object.values(this._locationMarkers).map((item) => item.getLatLng())
);
this._leafletMap.fitBounds(bounds.pad(0.5));
} }
public fitMarker(id: string): void { public fitMarker(id: string): void {
if (!this.map.leafletMap || !this._locationMarkers) { if (!this._leafletMap || !this._locationMarkers) {
return; return;
} }
const marker = this._locationMarkers[id]; const marker = this._locationMarkers[id];
@@ -88,44 +90,29 @@ export class HaLocationsEditor extends LitElement {
return; return;
} }
if ("getBounds" in marker) { if ("getBounds" in marker) {
this.map.leafletMap.fitBounds(marker.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];
if (circle) { if (circle) {
this.map.leafletMap.fitBounds(circle.getBounds()); this._leafletMap.fitBounds(circle.getBounds());
} else { } else {
this.map.leafletMap.setView(marker.getLatLng(), this.zoom); this._leafletMap.setView(marker.getLatLng(), this.fitZoom);
} }
} }
} }
protected render(): TemplateResult { protected render(): TemplateResult {
return html`<ha-map return html` <div id="map"></div> `;
.hass=${this.hass}
.layers=${this._getLayers(this._circles, this._locationMarkers)}
.zoom=${this.zoom}
.autoFit=${this.autoFit}
.darkMode=${this.darkMode}
></ha-map>`;
} }
private _getLayers = memoizeOne( protected firstUpdated(changedProps: PropertyValues): void {
( super.firstUpdated(changedProps);
circles: Record<string, Circle>, this._initMap();
markers?: Record<string, Marker | Circle> }
): Array<Marker | Circle> => {
const layers: Array<Marker | Circle> = [];
Array.prototype.push.apply(layers, Object.values(circles));
if (markers) {
Array.prototype.push.apply(layers, Object.values(markers));
}
return layers;
}
);
public willUpdate(changedProps: PropertyValues): void { protected updated(changedProps: PropertyValues): void {
super.willUpdate(changedProps); super.updated(changedProps);
// Still loading. // Still loading.
if (!this.Leaflet) { if (!this.Leaflet) {
@@ -135,6 +122,37 @@ export class HaLocationsEditor extends LitElement {
if (changedProps.has("locations")) { if (changedProps.has("locations")) {
this._updateMarkers(); this._updateMarkers();
} }
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
return;
}
if (!this._leafletMap || !this._tileLayer) {
return;
}
this._tileLayer = replaceTileLayer(
this.Leaflet,
this._leafletMap,
this._tileLayer,
this.hass.themes.darkMode
);
}
}
private get _mapEl(): HTMLDivElement {
return this.shadowRoot!.querySelector("div")!;
}
private async _initMap(): Promise<void> {
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
this._mapEl,
this.hass.themes.darkMode,
true
);
this._updateMarkers();
this.fitMap();
this._leafletMap.invalidateSize();
} }
private _updateLocation(ev: DragEndEvent) { private _updateLocation(ev: DragEndEvent) {
@@ -171,18 +189,21 @@ export class HaLocationsEditor extends LitElement {
} }
private _updateMarkers(): void { private _updateMarkers(): void {
if (!this.locations || !this.locations.length) { if (this._locationMarkers) {
this._circles = {}; Object.values(this._locationMarkers).forEach((marker) => {
marker.remove();
});
this._locationMarkers = undefined; this._locationMarkers = undefined;
Object.values(this._circles).forEach((circle) => circle.remove());
this._circles = {};
}
if (!this.locations || !this.locations.length) {
return; return;
} }
const locationMarkers = {}; this._locationMarkers = {};
const circles = {};
const defaultZoneRadiusColor = getComputedStyle(this).getPropertyValue(
"--accent-color"
);
this.locations.forEach((location: MarkerLocation) => { this.locations.forEach((location: MarkerLocation) => {
let icon: DivIcon | undefined; let icon: DivIcon | undefined;
@@ -207,46 +228,45 @@ export class HaLocationsEditor extends LitElement {
const circle = this.Leaflet!.circle( const circle = this.Leaflet!.circle(
[location.latitude, location.longitude], [location.latitude, location.longitude],
{ {
color: location.radius_color || defaultZoneRadiusColor, color: location.radius_color || defaultRadiusColor,
radius: location.radius, radius: location.radius,
} }
); );
circle.addTo(this._leafletMap!);
if (location.radius_editable || location.location_editable) { if (location.radius_editable || location.location_editable) {
// @ts-ignore // @ts-ignore
circle.editing.enable(); circle.editing.enable();
circle.addEventListener("add", () => { // @ts-ignore
// @ts-ignore const moveMarker = circle.editing._moveMarker;
const moveMarker = circle.editing._moveMarker; // @ts-ignore
// @ts-ignore const resizeMarker = circle.editing._resizeMarkers[0];
const resizeMarker = circle.editing._resizeMarkers[0]; if (icon) {
if (icon) { moveMarker.setIcon(icon);
moveMarker.setIcon(icon); }
} resizeMarker.id = moveMarker.id = location.id;
resizeMarker.id = moveMarker.id = location.id; moveMarker
moveMarker .addEventListener(
.addEventListener( "dragend",
"dragend", // @ts-ignore
// @ts-ignore (ev: DragEndEvent) => this._updateLocation(ev)
(ev: DragEndEvent) => this._updateLocation(ev) )
) .addEventListener(
.addEventListener( "click",
"click", // @ts-ignore
// @ts-ignore (ev: MouseEvent) => this._markerClicked(ev)
(ev: MouseEvent) => this._markerClicked(ev) );
); if (location.radius_editable) {
if (location.radius_editable) { resizeMarker.addEventListener(
resizeMarker.addEventListener( "dragend",
"dragend", // @ts-ignore
// @ts-ignore (ev: DragEndEvent) => this._updateRadius(ev)
(ev: DragEndEvent) => this._updateRadius(ev) );
); } else {
} else { resizeMarker.remove();
resizeMarker.remove(); }
} this._locationMarkers![location.id] = circle;
});
locationMarkers[location.id] = circle;
} else { } else {
circles[location.id] = circle; this._circles[location.id] = circle;
} }
} }
if ( if (
@@ -255,7 +275,6 @@ export class HaLocationsEditor extends LitElement {
) { ) {
const options: MarkerOptions = { const options: MarkerOptions = {
title: location.name, title: location.name,
draggable: location.location_editable,
}; };
if (icon) { if (icon) {
@@ -274,14 +293,13 @@ export class HaLocationsEditor extends LitElement {
"click", "click",
// @ts-ignore // @ts-ignore
(ev: MouseEvent) => this._markerClicked(ev) (ev: MouseEvent) => this._markerClicked(ev)
); )
.addTo(this._leafletMap!);
(marker as any).id = location.id; (marker as any).id = location.id;
locationMarkers[location.id] = marker; this._locationMarkers![location.id] = marker;
} }
}); });
this._circles = circles;
this._locationMarkers = locationMarkers;
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
@@ -290,9 +308,23 @@ export class HaLocationsEditor extends LitElement {
display: block; display: block;
height: 300px; height: 300px;
} }
ha-map { #map {
height: 100%; height: 100%;
} }
.leaflet-marker-draggable {
cursor: move !important;
}
.leaflet-edit-resize {
border-radius: 50%;
cursor: nesw-resize !important;
}
.named-icon {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
}
`; `;
} }
} }

View File

@@ -1,15 +1,13 @@
import { Circle, Layer, Map, Marker, TileLayer } from "leaflet";
import { import {
Circle, css,
CircleMarker, CSSResultGroup,
LatLngTuple, html,
Layer, LitElement,
Map, PropertyValues,
Marker, TemplateResult,
Polyline, } from "lit";
TileLayer, import { customElement, property } from "lit/decorators";
} from "leaflet";
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { import {
LeafletModuleType, LeafletModuleType,
replaceTileLayer, replaceTileLayer,
@@ -17,324 +15,194 @@ import {
} from "../../common/dom/setup-leaflet-map"; } from "../../common/dom/setup-leaflet-map";
import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import "./ha-entity-marker"; import { debounce } from "../../common/util/debounce";
import "../../panels/map/ha-entity-marker";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-icon-button"; import "../ha-icon-button";
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
const getEntityId = (entity: string | HaMapEntity): string =>
typeof entity === "string" ? entity : entity.entity_id;
export interface HaMapPaths {
points: LatLngTuple[];
color?: string;
gradualOpacity?: number;
}
export interface HaMapEntity {
entity_id: string;
color: string;
}
@customElement("ha-map") @customElement("ha-map")
export class HaMap extends ReactiveElement { class HaMap extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entities?: string[] | HaMapEntity[]; @property() public entities?: string[];
@property({ attribute: false }) public paths?: HaMapPaths[]; @property() public darkMode?: boolean;
@property({ attribute: false }) public layers?: Layer[]; @property() public zoom?: number;
@property({ type: Boolean }) public autoFit = false;
@property({ type: Boolean }) public fitZones?: boolean;
@property({ type: Boolean }) public darkMode?: boolean;
@property({ type: Number }) public zoom = 14;
@state() private _loaded = false;
public leafletMap?: Map;
// eslint-disable-next-line
private Leaflet?: LeafletModuleType; private Leaflet?: LeafletModuleType;
private _leafletMap?: Map;
private _tileLayer?: TileLayer; private _tileLayer?: TileLayer;
// @ts-ignore
private _resizeObserver?: ResizeObserver; private _resizeObserver?: ResizeObserver;
private _debouncedResizeListener = debounce(
() => {
if (!this._leafletMap) {
return;
}
this._leafletMap.invalidateSize();
},
100,
false
);
private _mapItems: Array<Marker | Circle> = []; private _mapItems: Array<Marker | Circle> = [];
private _mapZones: Array<Marker | Circle> = []; private _mapZones: Array<Marker | Circle> = [];
private _mapPaths: Array<Polyline | CircleMarker> = []; private _connected = false;
public connectedCallback(): void { public connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
this._loadMap(); this._connected = true;
this._attachObserver(); if (this.hasUpdated) {
this.loadMap();
this._attachObserver();
}
} }
public disconnectedCallback(): void { public disconnectedCallback(): void {
super.disconnectedCallback(); super.disconnectedCallback();
if (this.leafletMap) { this._connected = false;
this.leafletMap.remove();
this.leafletMap = undefined; if (this._leafletMap) {
this._leafletMap.remove();
this._leafletMap = undefined;
this.Leaflet = undefined; this.Leaflet = undefined;
} }
this._loaded = false;
if (this._resizeObserver) { if (this._resizeObserver) {
this._resizeObserver.unobserve(this); this._resizeObserver.unobserve(this._mapEl);
} else {
window.removeEventListener("resize", this._debouncedResizeListener);
} }
} }
protected update(changedProps: PropertyValues) { protected render(): TemplateResult {
super.update(changedProps); if (!this.entities) {
return html``;
if (!this._loaded) {
return;
} }
return html` <div id="map"></div> `;
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this.loadMap();
if (this._connected) {
this._attachObserver();
}
}
protected shouldUpdate(changedProps) {
if (!changedProps.has("hass") || changedProps.size > 1) {
return true;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined; const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (changedProps.has("_loaded") || changedProps.has("entities")) { if (!oldHass || !this.entities) {
this._drawEntities(); return true;
} else if (this._loaded && oldHass && this.entities) { }
// Check if any state has changed
for (const entity of this.entities) { // Check if any state has changed
if ( for (const entity of this.entities) {
oldHass.states[getEntityId(entity)] !== if (oldHass.states[entity] !== this.hass!.states[entity]) {
this.hass!.states[getEntityId(entity)] return true;
) {
this._drawEntities();
break;
}
} }
} }
if (changedProps.has("_loaded") || changedProps.has("paths")) { return false;
this._drawPaths();
}
if (changedProps.has("_loaded") || changedProps.has("layers")) {
this._drawLayers(changedProps.get("layers") as Layer[] | undefined);
}
if (
changedProps.has("_loaded") ||
((changedProps.has("entities") || changedProps.has("layers")) &&
this.autoFit)
) {
this.fitMap();
}
if (changedProps.has("zoom")) {
this.leafletMap!.setZoom(this.zoom);
}
if (
!changedProps.has("darkMode") &&
(!changedProps.has("hass") ||
(oldHass && oldHass.themes.darkMode === this.hass.themes.darkMode))
) {
return;
}
const darkMode = this.darkMode ?? this.hass.themes.darkMode;
this._tileLayer = replaceTileLayer(
this.Leaflet!,
this.leafletMap!,
this._tileLayer!,
darkMode
);
this.shadowRoot!.getElementById("map")!.classList.toggle("dark", darkMode);
} }
private async _loadMap(): Promise<void> { protected updated(changedProps: PropertyValues): void {
let map = this.shadowRoot!.getElementById("map"); if (changedProps.has("hass")) {
if (!map) { this._drawEntities();
map = document.createElement("div"); this._fitMap();
map.id = "map";
this.shadowRoot!.append(map); const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
return;
}
if (!this.Leaflet || !this._leafletMap || !this._tileLayer) {
return;
}
this._tileLayer = replaceTileLayer(
this.Leaflet,
this._leafletMap,
this._tileLayer,
this.hass.themes.darkMode
);
} }
const darkMode = this.darkMode ?? this.hass.themes.darkMode;
[this.leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
map,
darkMode
);
this.shadowRoot!.getElementById("map")!.classList.toggle("dark", darkMode);
this._loaded = true;
} }
public fitMap(): void { private get _mapEl(): HTMLDivElement {
if (!this.leafletMap || !this.Leaflet || !this.hass) { return this.shadowRoot!.getElementById("map") as HTMLDivElement;
}
private async loadMap(): Promise<void> {
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
this._mapEl,
this.darkMode ?? this.hass.themes.darkMode
);
this._drawEntities();
this._leafletMap.invalidateSize();
this._fitMap();
}
private _fitMap(): void {
if (!this._leafletMap || !this.Leaflet || !this.hass) {
return; return;
} }
if (this._mapItems.length === 0) {
if (!this._mapItems.length && !this.layers?.length) { this._leafletMap.setView(
this.leafletMap.setView(
new this.Leaflet.LatLng( new this.Leaflet.LatLng(
this.hass.config.latitude, this.hass.config.latitude,
this.hass.config.longitude this.hass.config.longitude
), ),
this.zoom this.zoom || 14
); );
return; return;
} }
let bounds = this.Leaflet.latLngBounds( const bounds = this.Leaflet.latLngBounds(
this._mapItems ? this._mapItems.map((item) => item.getLatLng()) : [] this._mapItems ? this._mapItems.map((item) => item.getLatLng()) : []
); );
this._leafletMap.fitBounds(bounds.pad(0.5));
if (this.fitZones) { if (this.zoom && this._leafletMap.getZoom() > this.zoom) {
this._mapZones?.forEach((zone) => { this._leafletMap.setZoom(this.zoom);
bounds.extend(
"getBounds" in zone ? zone.getBounds() : zone.getLatLng()
);
});
} }
this.layers?.forEach((layer: any) => {
bounds.extend(
"getBounds" in layer ? layer.getBounds() : layer.getLatLng()
);
});
if (!this.layers) {
bounds = bounds.pad(0.5);
}
this.leafletMap.fitBounds(bounds, { maxZoom: this.zoom });
}
private _drawLayers(prevLayers: Layer[] | undefined): void {
if (prevLayers) {
prevLayers.forEach((layer) => layer.remove());
}
if (!this.layers) {
return;
}
const map = this.leafletMap!;
this.layers.forEach((layer) => {
map.addLayer(layer);
});
}
private _drawPaths(): void {
const hass = this.hass;
const map = this.leafletMap;
const Leaflet = this.Leaflet;
if (!hass || !map || !Leaflet) {
return;
}
if (this._mapPaths.length) {
this._mapPaths.forEach((marker) => marker.remove());
this._mapPaths = [];
}
if (!this.paths) {
return;
}
const darkPrimaryColor = getComputedStyle(this).getPropertyValue(
"--dark-primary-color"
);
this.paths.forEach((path) => {
let opacityStep: number;
let baseOpacity: number;
if (path.gradualOpacity) {
opacityStep = path.gradualOpacity / (path.points.length - 2);
baseOpacity = 1 - path.gradualOpacity;
}
for (
let pointIndex = 0;
pointIndex < path.points.length - 1;
pointIndex++
) {
const opacity = path.gradualOpacity
? baseOpacity! + pointIndex * opacityStep!
: undefined;
// DRAW point
this._mapPaths.push(
Leaflet!.circleMarker(path.points[pointIndex], {
radius: 3,
color: path.color || darkPrimaryColor,
opacity,
fillOpacity: opacity,
interactive: false,
})
);
// DRAW line between this and next point
this._mapPaths.push(
Leaflet!.polyline(
[path.points[pointIndex], path.points[pointIndex + 1]],
{
color: path.color || darkPrimaryColor,
opacity,
interactive: false,
}
)
);
}
const pointIndex = path.points.length - 1;
if (pointIndex >= 0) {
const opacity = path.gradualOpacity
? baseOpacity! + pointIndex * opacityStep!
: undefined;
// DRAW end path point
this._mapPaths.push(
Leaflet!.circleMarker(path.points[pointIndex], {
radius: 3,
color: path.color || darkPrimaryColor,
opacity,
fillOpacity: opacity,
interactive: false,
})
);
}
this._mapPaths.forEach((marker) => map.addLayer(marker));
});
} }
private _drawEntities(): void { private _drawEntities(): void {
const hass = this.hass; const hass = this.hass;
const map = this.leafletMap; const map = this._leafletMap;
const Leaflet = this.Leaflet; const Leaflet = this.Leaflet;
if (!hass || !map || !Leaflet) { if (!hass || !map || !Leaflet) {
return; return;
} }
if (this._mapItems.length) { if (this._mapItems) {
this._mapItems.forEach((marker) => marker.remove()); this._mapItems.forEach((marker) => marker.remove());
this._mapItems = [];
} }
const mapItems: Layer[] = (this._mapItems = []);
if (this._mapZones.length) { if (this._mapZones) {
this._mapZones.forEach((marker) => marker.remove()); this._mapZones.forEach((marker) => marker.remove());
this._mapZones = [];
} }
const mapZones: Layer[] = (this._mapZones = []);
if (!this.entities) { const allEntities = this.entities!.concat();
return;
}
const computedStyles = getComputedStyle(this); for (const entity of allEntities) {
const zoneColor = computedStyles.getPropertyValue("--accent-color"); const entityId = entity;
const darkPrimaryColor = computedStyles.getPropertyValue( const stateObj = hass.states[entityId];
"--dark-primary-color"
);
const className =
this.darkMode ?? this.hass.themes.darkMode ? "dark" : "light";
for (const entity of this.entities) {
const stateObj = hass.states[getEntityId(entity)];
if (!stateObj) { if (!stateObj) {
continue; continue;
} }
@@ -372,12 +240,13 @@ export class HaMap extends ReactiveElement {
} }
// create marker with the icon // create marker with the icon
this._mapZones.push( mapZones.push(
Leaflet.marker([latitude, longitude], { Leaflet.marker([latitude, longitude], {
icon: Leaflet.divIcon({ icon: Leaflet.divIcon({
html: iconHTML, html: iconHTML,
iconSize: [24, 24], iconSize: [24, 24],
className, className:
this.darkMode ?? this.hass.themes.darkMode ? "dark" : "light",
}), }),
interactive: false, interactive: false,
title, title,
@@ -385,10 +254,10 @@ export class HaMap extends ReactiveElement {
); );
// create circle around it // create circle around it
this._mapZones.push( mapZones.push(
Leaflet.circle([latitude, longitude], { Leaflet.circle([latitude, longitude], {
interactive: false, interactive: false,
color: zoneColor, color: "#FF9800",
radius, radius,
}) })
); );
@@ -404,20 +273,17 @@ export class HaMap extends ReactiveElement {
.join("") .join("")
.substr(0, 3); .substr(0, 3);
// create marker with the icon // create market with the icon
this._mapItems.push( mapItems.push(
Leaflet.marker([latitude, longitude], { Leaflet.marker([latitude, longitude], {
icon: Leaflet.divIcon({ icon: Leaflet.divIcon({
// Leaflet clones this element before adding it to the map. This messes up
// our Polymer object and we can't pass data through. Thus we hack like this.
html: ` html: `
<ha-entity-marker <ha-entity-marker
entity-id="${getEntityId(entity)}" entity-id="${entityId}"
entity-name="${entityName}" entity-name="${entityName}"
entity-picture="${entityPicture || ""}" entity-picture="${entityPicture || ""}"
${
typeof entity !== "string"
? `entity-color="${entity.color}"`
: ""
}
></ha-entity-marker> ></ha-entity-marker>
`, `,
iconSize: [48, 48], iconSize: [48, 48],
@@ -429,10 +295,10 @@ export class HaMap extends ReactiveElement {
// create circle around if entity has accuracy // create circle around if entity has accuracy
if (gpsAccuracy) { if (gpsAccuracy) {
this._mapItems.push( mapItems.push(
Leaflet.circle([latitude, longitude], { Leaflet.circle([latitude, longitude], {
interactive: false, interactive: false,
color: darkPrimaryColor, color: "#0288D1",
radius: gpsAccuracy, radius: gpsAccuracy,
}) })
); );
@@ -443,14 +309,20 @@ export class HaMap extends ReactiveElement {
this._mapZones.forEach((marker) => map.addLayer(marker)); this._mapZones.forEach((marker) => map.addLayer(marker));
} }
private async _attachObserver(): Promise<void> { private _attachObserver(): void {
if (!this._resizeObserver) { // Observe changes to map size and invalidate to prevent broken rendering
await installResizeObserver(); // Uses ResizeObserver in Chrome, otherwise window resize event
this._resizeObserver = new ResizeObserver(() => {
this.leafletMap?.invalidateSize({ debounceMoveend: true }); // @ts-ignore
}); if (typeof ResizeObserver === "function") {
// @ts-ignore
this._resizeObserver = new ResizeObserver(() =>
this._debouncedResizeListener()
);
this._resizeObserver.observe(this._mapEl);
} else {
window.addEventListener("resize", this._debouncedResizeListener);
} }
this._resizeObserver.observe(this);
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
@@ -465,25 +337,13 @@ export class HaMap extends ReactiveElement {
#map.dark { #map.dark {
background: #090909; background: #090909;
} }
.light {
color: #000000;
}
.dark { .dark {
color: #ffffff; color: #ffffff;
} }
.leaflet-marker-draggable {
cursor: move !important; .light {
} color: #000000;
.leaflet-edit-resize {
border-radius: 50%;
cursor: nesw-resize !important;
}
.named-icon {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
} }
`; `;
} }

View File

@@ -45,6 +45,7 @@ import "../ha-button-menu";
import "../ha-card"; import "../ha-card";
import "../ha-circular-progress"; import "../ha-circular-progress";
import "../ha-fab"; import "../ha-fab";
import "../ha-paper-dropdown-menu";
import "../ha-svg-icon"; import "../ha-svg-icon";
declare global { declare global {

View File

@@ -377,10 +377,7 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
major: { major: {
fontStyle: "bold", fontStyle: "bold",
}, },
source: "auto",
sampleSize: 5,
autoSkipPadding: 20, autoSkipPadding: 20,
maxRotation: 0,
}, },
}, },
], ],

View File

@@ -236,9 +236,7 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
major: { major: {
fontStyle: "bold", fontStyle: "bold",
}, },
sampleSize: 5,
autoSkipPadding: 50, autoSkipPadding: 50,
maxRotation: 0,
}, },
categoryPercentage: undefined, categoryPercentage: undefined,
barPercentage: undefined, barPercentage: undefined,

View File

@@ -12,20 +12,20 @@ export interface ConfigEntry {
| "setup_retry" | "setup_retry"
| "not_loaded" | "not_loaded"
| "failed_unload"; | "failed_unload";
connection_class: string;
supports_options: boolean; supports_options: boolean;
supports_unload: boolean; supports_unload: boolean;
pref_disable_new_entities: boolean;
pref_disable_polling: boolean;
disabled_by: "user" | null; disabled_by: "user" | null;
reason: string | null; reason: string | null;
} }
export type ConfigEntryMutableParams = Partial< export interface ConfigEntryMutableParams {
Pick< title: string;
ConfigEntry, }
"title" | "pref_disable_new_entities" | "pref_disable_polling"
> export interface ConfigEntrySystemOptions {
>; disable_new_entities: boolean;
}
export const getConfigEntries = (hass: HomeAssistant) => export const getConfigEntries = (hass: HomeAssistant) =>
hass.callApi<ConfigEntry[]>("GET", "config/config_entries/entry"); hass.callApi<ConfigEntry[]>("GET", "config/config_entries/entry");
@@ -33,9 +33,9 @@ export const getConfigEntries = (hass: HomeAssistant) =>
export const updateConfigEntry = ( export const updateConfigEntry = (
hass: HomeAssistant, hass: HomeAssistant,
configEntryId: string, configEntryId: string,
updatedValues: ConfigEntryMutableParams updatedValues: Partial<ConfigEntryMutableParams>
) => ) =>
hass.callWS<{ require_restart: boolean; config_entry: ConfigEntry }>({ hass.callWS<ConfigEntry>({
type: "config_entries/update", type: "config_entries/update",
entry_id: configEntryId, entry_id: configEntryId,
...updatedValues, ...updatedValues,
@@ -51,15 +51,13 @@ export const reloadConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
require_restart: boolean; require_restart: boolean;
}>("POST", `config/config_entries/entry/${configEntryId}/reload`); }>("POST", `config/config_entries/entry/${configEntryId}/reload`);
export interface DisableConfigEntryResult {
require_restart: boolean;
}
export const disableConfigEntry = ( export const disableConfigEntry = (
hass: HomeAssistant, hass: HomeAssistant,
configEntryId: string configEntryId: string
) => ) =>
hass.callWS<DisableConfigEntryResult>({ hass.callWS<{
require_restart: boolean;
}>({
type: "config_entries/disable", type: "config_entries/disable",
entry_id: configEntryId, entry_id: configEntryId,
disabled_by: "user", disabled_by: "user",
@@ -73,3 +71,23 @@ export const enableConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
entry_id: configEntryId, entry_id: configEntryId,
disabled_by: null, disabled_by: null,
}); });
export const getConfigEntrySystemOptions = (
hass: HomeAssistant,
configEntryId: string
) =>
hass.callWS<ConfigEntrySystemOptions>({
type: "config_entries/system_options/list",
entry_id: configEntryId,
});
export const updateConfigEntrySystemOptions = (
hass: HomeAssistant,
configEntryId: string,
params: Partial<ConfigEntrySystemOptions>
) =>
hass.callWS({
type: "config_entries/system_options/update",
entry_id: configEntryId,
...params,
});

View File

@@ -212,15 +212,13 @@ export const setHassioAddonOption = async (
export const validateHassioAddonOption = async ( export const validateHassioAddonOption = async (
hass: HomeAssistant, hass: HomeAssistant,
slug: string, slug: string
data?: any
): Promise<{ message: string; valid: boolean }> => { ): Promise<{ message: string; valid: boolean }> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) { if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return hass.callWS({ return hass.callWS({
type: "supervisor/api", type: "supervisor/api",
endpoint: `/addons/${slug}/options/validate`, endpoint: `/addons/${slug}/options/validate`,
method: "post", method: "post",
data,
}); });
} }

View File

@@ -14,17 +14,12 @@ interface HassioHardwareAudioList {
}; };
} }
interface HardwareDevice {
attributes: Record<string, string>;
by_id: null | string;
dev_path: string;
name: string;
subsystem: string;
sysfs: string;
}
export interface HassioHardwareInfo { export interface HassioHardwareInfo {
devices: HardwareDevice[]; serial: string[];
input: string[];
disk: string[];
gpio: string[];
audio: Record<string, unknown>;
} }
export const fetchHassioHardwareAudio = async ( export const fetchHassioHardwareAudio = async (

View File

@@ -41,7 +41,6 @@ export interface HassioSnapshotDetail extends HassioSnapshot {
export interface HassioFullSnapshotCreateParams { export interface HassioFullSnapshotCreateParams {
name: string; name: string;
password?: string; password?: string;
confirm_password?: string;
} }
export interface HassioPartialSnapshotCreateParams export interface HassioPartialSnapshotCreateParams
extends HassioFullSnapshotCreateParams { extends HassioFullSnapshotCreateParams {
@@ -131,21 +130,6 @@ export const createHassioFullSnapshot = async (
); );
}; };
export const removeSnapshot = async (hass: HomeAssistant, slug: string) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: `/snapshots/${slug}/remove`,
method: "post",
});
return;
}
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/snapshots/${slug}/remove`
);
};
export const createHassioPartialSnapshot = async ( export const createHassioPartialSnapshot = async (
hass: HomeAssistant, hass: HomeAssistant,
data: HassioPartialSnapshotCreateParams data: HassioPartialSnapshotCreateParams

View File

@@ -36,16 +36,18 @@ export const getIcon = (iconName: string) =>
.then((icon) => resolve_(icon)) .then((icon) => resolve_(icon))
.catch((e) => reject_(e)); .catch((e) => reject_(e));
} }
toRead = [];
}) })
).catch((e) => { )
// Firefox in private mode doesn't support IDB .catch((e) => {
// Safari sometime doesn't open the DB so we time out // Firefox in private mode doesn't support IDB
for (const [, , reject_] of toRead) { // Safari sometime doesn't open the DB so we time out
reject_(e); for (const [, , reject_] of toRead) {
} reject_(e);
toRead = []; }
}); })
.finally(() => {
toRead = [];
});
}); });
export const findIconChunk = (icon: string): string => { export const findIconChunk = (icon: string): string => {

View File

@@ -2,7 +2,6 @@ import { Connection, getCollection } from "home-assistant-js-websocket";
import { Store } from "home-assistant-js-websocket/dist/store"; import { Store } from "home-assistant-js-websocket/dist/store";
import { LocalizeFunc } from "../../common/translations/localize"; import { LocalizeFunc } from "../../common/translations/localize";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { fetchFrontendUserData, saveFrontendUserData } from "../frontend";
import { HassioAddonsInfo } from "../hassio/addon"; import { HassioAddonsInfo } from "../hassio/addon";
import { HassioHassOSInfo, HassioHostInfo } from "../hassio/host"; import { HassioHassOSInfo, HassioHostInfo } from "../hassio/host";
import { NetworkInfo } from "../hassio/network"; import { NetworkInfo } from "../hassio/network";
@@ -14,28 +13,6 @@ import {
} from "../hassio/supervisor"; } from "../hassio/supervisor";
import { SupervisorStore } from "./store"; import { SupervisorStore } from "./store";
export interface SupervisorFrontendPrefrences {
snapshot_before_update: Record<string, boolean>;
}
declare global {
interface FrontendUserData {
supervisor: SupervisorFrontendPrefrences;
}
}
export const fetchSupervisorFrontendPreferences = async (
hass: HomeAssistant
): Promise<SupervisorFrontendPrefrences> => {
const stored = await fetchFrontendUserData(hass.connection, "supervisor");
return stored || { snapshot_before_update: {} };
};
export const saveSupervisorFrontendPreferences = (
hass: HomeAssistant,
data: SupervisorFrontendPrefrences
) => saveFrontendUserData(hass.connection, "supervisor", data);
export const supervisorWSbaseCommand = { export const supervisorWSbaseCommand = {
type: "supervisor/api", type: "supervisor/api",
method: "GET", method: "GET",

View File

@@ -1,6 +1,14 @@
import { navigate } from "../common/navigate"; import { navigate } from "../common/navigate";
import {
DEFAULT_ACCENT_COLOR,
DEFAULT_PRIMARY_COLOR,
} from "../resources/ha-style";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
export const defaultRadiusColor = DEFAULT_ACCENT_COLOR;
export const homeRadiusColor = DEFAULT_PRIMARY_COLOR;
export const passiveRadiusColor = "#9b9b9b";
export interface Zone { export interface Zone {
id: string; id: string;
name: string; name: string;

View File

@@ -3,17 +3,17 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeRTLDirection } from "../../common/util/compute_rtl"; import { computeRTLDirection } from "../../common/util/compute_rtl";
import "../../components/ha-circular-progress";
import "../../components/ha-dialog"; import "../../components/ha-dialog";
import "../../components/ha-formfield"; import "../../components/ha-formfield";
import "../../components/ha-switch"; import "../../components/ha-switch";
import type { HaSwitch } from "../../components/ha-switch"; import type { HaSwitch } from "../../components/ha-switch";
import { import {
ConfigEntryMutableParams, getConfigEntrySystemOptions,
updateConfigEntry, updateConfigEntrySystemOptions,
} from "../../data/config_entries"; } from "../../data/config_entries";
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { showAlertDialog } from "../generic/show-dialog-box";
import { ConfigEntrySystemOptionsDialogParams } from "./show-dialog-config-entry-system-options"; import { ConfigEntrySystemOptionsDialogParams } from "./show-dialog-config-entry-system-options";
@customElement("dialog-config-entry-system-options") @customElement("dialog-config-entry-system-options")
@@ -22,12 +22,12 @@ class DialogConfigEntrySystemOptions extends LitElement {
@state() private _disableNewEntities!: boolean; @state() private _disableNewEntities!: boolean;
@state() private _disablePolling!: boolean;
@state() private _error?: string; @state() private _error?: string;
@state() private _params?: ConfigEntrySystemOptionsDialogParams; @state() private _params?: ConfigEntrySystemOptionsDialogParams;
@state() private _loading = false;
@state() private _submitting = false; @state() private _submitting = false;
public async showDialog( public async showDialog(
@@ -35,8 +35,13 @@ class DialogConfigEntrySystemOptions extends LitElement {
): Promise<void> { ): Promise<void> {
this._params = params; this._params = params;
this._error = undefined; this._error = undefined;
this._disableNewEntities = params.entry.pref_disable_new_entities; this._loading = true;
this._disablePolling = params.entry.pref_disable_polling; const systemOptions = await getConfigEntrySystemOptions(
this.hass,
params.entry.entry_id
);
this._loading = false;
this._disableNewEntities = systemOptions.disable_new_entities;
} }
public closeDialog(): void { public closeDialog(): void {
@@ -61,57 +66,45 @@ class DialogConfigEntrySystemOptions extends LitElement {
this._params.entry.domain this._params.entry.domain
)} )}
> >
${this._error ? html` <div class="error">${this._error}</div> ` : ""} <div>
<ha-formfield ${this._loading
.label=${html`<p> ? html`
${this.hass.localize( <div class="init-spinner">
"ui.dialogs.config_entry_system_options.enable_new_entities_label" <ha-circular-progress active></ha-circular-progress>
)} </div>
</p> `
<p class="secondary"> : html`
${this.hass.localize( ${this._error
"ui.dialogs.config_entry_system_options.enable_new_entities_description", ? html` <div class="error">${this._error}</div> `
"integration", : ""}
this.hass.localize( <div class="form">
`component.${this._params.entry.domain}.title` <ha-formfield
) || this._params.entry.domain .label=${html`<p>
)} ${this.hass.localize(
</p>`} "ui.dialogs.config_entry_system_options.enable_new_entities_label"
.dir=${computeRTLDirection(this.hass)} )}
> </p>
<ha-switch <p class="secondary">
.checked=${!this._disableNewEntities} ${this.hass.localize(
@change=${this._disableNewEntitiesChanged} "ui.dialogs.config_entry_system_options.enable_new_entities_description",
.disabled=${this._submitting} "integration",
></ha-switch> this.hass.localize(
</ha-formfield> `component.${this._params.entry.domain}.title`
${this._allowUpdatePolling() ) || this._params.entry.domain
? html` )}
<ha-formfield </p>`}
.label=${html`<p> .dir=${computeRTLDirection(this.hass)}
${this.hass.localize( >
"ui.dialogs.config_entry_system_options.enable_polling_label" <ha-switch
)} .checked=${!this._disableNewEntities}
</p> @change=${this._disableNewEntitiesChanged}
<p class="secondary"> .disabled=${this._submitting}
${this.hass.localize( >
"ui.dialogs.config_entry_system_options.enable_polling_description", </ha-switch>
"integration", </ha-formfield>
this.hass.localize( </div>
`component.${this._params.entry.domain}.title` `}
) || this._params.entry.domain </div>
)}
</p>`}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.checked=${!this._disablePolling}
@change=${this._disablePollingChanged}
.disabled=${this._submitting}
></ha-switch>
</ha-formfield>
`
: ""}
<mwc-button <mwc-button
slot="secondaryAction" slot="secondaryAction"
@click=${this.closeDialog} @click=${this.closeDialog}
@@ -122,7 +115,7 @@ class DialogConfigEntrySystemOptions extends LitElement {
<mwc-button <mwc-button
slot="primaryAction" slot="primaryAction"
@click="${this._updateEntry}" @click="${this._updateEntry}"
.disabled=${this._submitting} .disabled=${this._submitting || this._loading}
> >
${this.hass.localize("ui.dialogs.config_entry_system_options.update")} ${this.hass.localize("ui.dialogs.config_entry_system_options.update")}
</mwc-button> </mwc-button>
@@ -130,47 +123,22 @@ class DialogConfigEntrySystemOptions extends LitElement {
`; `;
} }
private _allowUpdatePolling() {
return (
this._params!.manifest &&
(this._params!.manifest.iot_class === "local_polling" ||
this._params!.manifest.iot_class === "cloud_polling")
);
}
private _disableNewEntitiesChanged(ev: Event): void { private _disableNewEntitiesChanged(ev: Event): void {
this._error = undefined; this._error = undefined;
this._disableNewEntities = !(ev.target as HaSwitch).checked; this._disableNewEntities = !(ev.target as HaSwitch).checked;
} }
private _disablePollingChanged(ev: Event): void {
this._error = undefined;
this._disablePolling = !(ev.target as HaSwitch).checked;
}
private async _updateEntry(): Promise<void> { private async _updateEntry(): Promise<void> {
this._submitting = true; this._submitting = true;
const data: ConfigEntryMutableParams = {
pref_disable_new_entities: this._disableNewEntities,
};
if (this._allowUpdatePolling()) {
data.pref_disable_polling = this._disablePolling;
}
try { try {
const result = await updateConfigEntry( await updateConfigEntrySystemOptions(
this.hass, this.hass,
this._params!.entry.entry_id, this._params!.entry.entry_id,
data {
disable_new_entities: this._disableNewEntities,
}
); );
if (result.require_restart) { this._params = undefined;
await showAlertDialog(this, {
text: this.hass.localize(
"ui.dialogs.config_entry_system_options.restart_home_assistant"
),
});
}
this._params!.entryUpdated(result.config_entry);
this.closeDialog();
} catch (err) { } catch (err) {
this._error = err.message || "Unknown error"; this._error = err.message || "Unknown error";
} finally { } finally {
@@ -182,6 +150,20 @@ class DialogConfigEntrySystemOptions extends LitElement {
return [ return [
haStyleDialog, haStyleDialog,
css` css`
.init-spinner {
padding: 50px 100px;
text-align: center;
}
.form {
padding-top: 6px;
padding-bottom: 24px;
color: var(--primary-text-color);
}
.secondary {
color: var(--secondary-text-color);
}
.error { .error {
color: var(--error-color); color: var(--error-color);
} }

View File

@@ -1,11 +1,12 @@
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { ConfigEntry } from "../../data/config_entries"; import { ConfigEntry } from "../../data/config_entries";
import { IntegrationManifest } from "../../data/integration";
export interface ConfigEntrySystemOptionsDialogParams { export interface ConfigEntrySystemOptionsDialogParams {
entry: ConfigEntry; entry: ConfigEntry;
manifest?: IntegrationManifest; // updateEntry: (
entryUpdated(entry: ConfigEntry): void; // updates: Partial<EntityRegistryEntryUpdateParams>
// ) => Promise<unknown>;
// removeEntry: () => Promise<boolean>;
} }
export const loadConfigEntrySystemOptionsDialog = () => export const loadConfigEntrySystemOptionsDialog = () =>

View File

@@ -108,28 +108,12 @@ export const showOptionsFlowDialog = (
`; `;
}, },
renderShowFormProgressHeader(hass, step) { renderShowFormProgressHeader(_hass, _step) {
return ( return "";
hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.title`
) || hass.localize(`component.${configEntry.domain}.title`)
);
}, },
renderShowFormProgressDescription(hass, step) { renderShowFormProgressDescription(_hass, _step) {
const description = hass.localize( return "";
`component.${configEntry.domain}.options.progress.${step.progress_action}`,
step.description_placeholders
);
return description
? html`
<ha-markdown
allowsvg
breaks
.content=${description}
></ha-markdown>
`
: "";
}, },
} }
); );

View File

@@ -0,0 +1,74 @@
import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { enableWrite } from "../common/auth/token_storage";
import LocalizeMixin from "../mixins/localize-mixin";
import "../styles/polymer-ha-style";
class HaStoreAuth extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style">
paper-card {
position: fixed;
padding: 8px 0;
bottom: 16px;
right: 16px;
}
.card-content {
color: var(--primary-text-color);
}
.card-actions {
text-align: right;
border-top: 0;
margin-right: -4px;
}
:host(.small) paper-card {
bottom: 0;
left: 0;
right: 0;
}
</style>
<paper-card elevation="4">
<div class="card-content">[[localize('ui.auth_store.ask')]]</div>
<div class="card-actions">
<mwc-button on-click="_done"
>[[localize('ui.auth_store.decline')]]</mwc-button
>
<mwc-button raised on-click="_save"
>[[localize('ui.auth_store.confirm')]]</mwc-button
>
</div>
</paper-card>
`;
}
static get properties() {
return {
hass: Object,
};
}
ready() {
super.ready();
this.classList.toggle("small", window.innerWidth < 600);
}
_save() {
enableWrite();
this._done();
}
_done() {
const card = this.shadowRoot.querySelector("paper-card");
card.style.transition = "bottom .25s";
card.style.bottom = `-${card.offsetHeight + 8}px`;
setTimeout(() => this.parentNode.removeChild(this), 300);
}
}
customElements.define("ha-store-auth-card", HaStoreAuth);

View File

@@ -1,78 +0,0 @@
import { LitElement, TemplateResult, html, css } from "lit";
import { property } from "lit/decorators";
import { enableWrite } from "../common/auth/token_storage";
import { HomeAssistant } from "../types";
import "../components/ha-card";
import type { HaCard } from "../components/ha-card";
import "@material/mwc-button/mwc-button";
class HaStoreAuth extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
protected render(): TemplateResult {
return html`
<ha-card>
<div class="card-content">
${this.hass.localize("ui.auth_store.ask")}
</div>
<div class="card-actions">
<mwc-button @click=${this._dismiss}>
${this.hass.localize("ui.auth_store.decline")}
</mwc-button>
<mwc-button raised @click=${this._save}>
${this.hass.localize("ui.auth_store.confirm")}
</mwc-button>
</div>
</ha-card>
`;
}
firstUpdated() {
this.classList.toggle("small", window.innerWidth < 600);
}
private _save(): void {
enableWrite();
this._dismiss();
}
private _dismiss(): void {
const card = this.shadowRoot!.querySelector("ha-card") as HaCard;
card.style.bottom = `-${card.offsetHeight + 8}px`;
setTimeout(() => this.parentNode!.removeChild(this), 300);
}
static get styles() {
return css`
ha-card {
position: fixed;
padding: 8px 0;
bottom: 16px;
right: 16px;
transition: bottom 0.25s;
--ha-card-box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2),
0px 6px 10px 0px rgba(0, 0, 0, 0.14),
0px 1px 18px 0px rgba(0, 0, 0, 0.12);
}
.card-actions {
text-align: right;
border-top: 0;
}
:host(.small) ha-card {
bottom: 0;
left: 0;
right: 0;
}
`;
}
}
customElements.define("ha-store-auth-card", HaStoreAuth);
declare global {
interface HTMLElementTagNameMap {
"ha-store-auth-card": HaStoreAuth;
}
}

View File

@@ -23,12 +23,16 @@ class MoreInfoPerson extends LitElement {
} }
return html` return html`
<ha-attributes
.hass=${this.hass}
.stateObj=${this.stateObj}
extra-filters="id,user_id,editable"
></ha-attributes>
${this.stateObj.attributes.latitude && this.stateObj.attributes.longitude ${this.stateObj.attributes.latitude && this.stateObj.attributes.longitude
? html` ? html`
<ha-map <ha-map
.hass=${this.hass} .hass=${this.hass}
.entities=${this._entityArray(this.stateObj.entity_id)} .entities=${this._entityArray(this.stateObj.entity_id)}
autoFit
></ha-map> ></ha-map>
` `
: ""} : ""}
@@ -47,11 +51,6 @@ class MoreInfoPerson extends LitElement {
</div> </div>
` `
: ""} : ""}
<ha-attributes
.hass=${this.hass}
.stateObj=${this.stateObj}
extra-filters="id,user_id,editable"
></ha-attributes>
`; `;
} }

View File

@@ -17,6 +17,11 @@ class MoreInfoTimer extends LitElement {
} }
return html` return html`
<ha-attributes
.hass=${this.hass}
.stateObj=${this.stateObj}
extra-filters="remaining"
></ha-attributes>
<div class="actions"> <div class="actions">
${this.stateObj.state === "idle" || this.stateObj.state === "paused" ${this.stateObj.state === "idle" || this.stateObj.state === "paused"
? html` ? html`
@@ -52,11 +57,6 @@ class MoreInfoTimer extends LitElement {
` `
: ""} : ""}
</div> </div>
<ha-attributes
.hass=${this.hass}
.stateObj=${this.stateObj}
extra-filters="remaining"
></ha-attributes>
`; `;
} }

View File

@@ -108,7 +108,7 @@ class MoreInfoWeather extends LitElement {
this.stateObj.attributes.pressure, this.stateObj.attributes.pressure,
this.hass.locale this.hass.locale
)} )}
${getWeatherUnit(this.hass, "pressure")} ${getWeatherUnit(this.hass, "air_pressure")}
</div> </div>
</div> </div>
` `

View File

@@ -5,7 +5,6 @@ import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { throttle } from "../../common/util/throttle"; import { throttle } from "../../common/util/throttle";
import "../../components/ha-circular-progress"; import "../../components/ha-circular-progress";
import "../../components/state-history-charts"; import "../../components/state-history-charts";
import { fetchUsers } from "../../data/user";
import { getLogbookData, LogbookEntry } from "../../data/logbook"; import { getLogbookData, LogbookEntry } from "../../data/logbook";
import { loadTraceContexts, TraceContexts } from "../../data/trace"; import { loadTraceContexts, TraceContexts } from "../../data/trace";
import "../../panels/logbook/ha-logbook"; import "../../panels/logbook/ha-logbook";
@@ -23,12 +22,10 @@ export class MoreInfoLogbook extends LitElement {
@state() private _traceContexts?: TraceContexts; @state() private _traceContexts?: TraceContexts;
@state() private _userIdToName = {}; @state() private _persons = {};
private _lastLogbookDate?: Date; private _lastLogbookDate?: Date;
private _fetchUserPromise?: Promise<void>;
private _throttleGetLogbookEntries = throttle(() => { private _throttleGetLogbookEntries = throttle(() => {
this._getLogBookData(); this._getLogBookData();
}, 10000); }, 10000);
@@ -62,7 +59,7 @@ export class MoreInfoLogbook extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.entries=${this._logbookEntries} .entries=${this._logbookEntries}
.traceContexts=${this._traceContexts} .traceContexts=${this._traceContexts}
.userIdToName=${this._userIdToName} .userIdToName=${this._persons}
></ha-logbook> ></ha-logbook>
` `
: html`<div class="no-entries"> : html`<div class="no-entries">
@@ -73,7 +70,7 @@ export class MoreInfoLogbook extends LitElement {
} }
protected firstUpdated(): void { protected firstUpdated(): void {
this._fetchUserPromise = this._fetchUserNames(); this._fetchPersonNames();
this.addEventListener("click", (ev) => { this.addEventListener("click", (ev) => {
if ((ev.composedPath()[0] as HTMLElement).tagName === "A") { if ((ev.composedPath()[0] as HTMLElement).tagName === "A") {
setTimeout(() => closeDialog("ha-more-info-dialog"), 500); setTimeout(() => closeDialog("ha-more-info-dialog"), 500);
@@ -128,7 +125,6 @@ export class MoreInfoLogbook extends LitElement {
true true
), ),
loadTraceContexts(this.hass), loadTraceContexts(this.hass),
this._fetchUserPromise,
]); ]);
this._logbookEntries = this._logbookEntries this._logbookEntries = this._logbookEntries
? [...newEntries, ...this._logbookEntries] ? [...newEntries, ...this._logbookEntries]
@@ -137,34 +133,16 @@ export class MoreInfoLogbook extends LitElement {
this._traceContexts = traceContexts; this._traceContexts = traceContexts;
} }
private async _fetchUserNames() { private _fetchPersonNames() {
const userIdToName = {};
// Start loading users
const userProm = this.hass.user?.is_admin && fetchUsers(this.hass);
// Process persons
Object.values(this.hass.states).forEach((entity) => { Object.values(this.hass.states).forEach((entity) => {
if ( if (
entity.attributes.user_id && entity.attributes.user_id &&
computeStateDomain(entity) === "person" computeStateDomain(entity) === "person"
) { ) {
this._userIdToName[entity.attributes.user_id] = this._persons[entity.attributes.user_id] =
entity.attributes.friendly_name; entity.attributes.friendly_name;
} }
}); });
// Process users
if (userProm) {
const users = await userProm;
for (const user of users) {
if (!(user.id in userIdToName)) {
userIdToName[user.id] = user.name;
}
}
}
this._userIdToName = userIdToName;
} }
static get styles() { static get styles() {

View File

@@ -1,4 +1,4 @@
import { Layout1d, scroll } from "../../resources/lit-virtualizer"; import { Layout1d, scroll } from "@lit-labs/virtualizer";
import "@material/mwc-list/mwc-list"; import "@material/mwc-list/mwc-list";
import type { List } from "@material/mwc-list/mwc-list"; import type { List } from "@material/mwc-list/mwc-list";
import { SingleSelectedEvent } from "@material/mwc-list/mwc-list-foundation"; import { SingleSelectedEvent } from "@material/mwc-list/mwc-list-foundation";
@@ -188,6 +188,7 @@ export class QuickBar extends LitElement {
${scroll({ ${scroll({
items, items,
layout: Layout1d, layout: Layout1d,
// @ts-expect-error
renderItem: (item: QuickBarItem, index) => renderItem: (item: QuickBarItem, index) =>
this._renderItem(item, index), this._renderItem(item, index),
})} })}
@@ -223,7 +224,7 @@ export class QuickBar extends LitElement {
private _renderItem(item: QuickBarItem, index?: number) { private _renderItem(item: QuickBarItem, index?: number) {
if (!item) { if (!item) {
return html``; return undefined;
} }
return isCommandItem(item) return isCommandItem(item)
? this._renderCommandItem(item, index) ? this._renderCommandItem(item, index)
@@ -638,6 +639,18 @@ export class QuickBar extends LitElement {
margin-left: 8px; margin-left: 8px;
} }
.uni-virtualizer-host {
display: block;
position: relative;
contain: strict;
overflow: auto;
height: 100%;
}
.uni-virtualizer-host > * {
box-sizing: border-box;
}
mwc-list-item { mwc-list-item {
width: 100%; width: 100%;
} }

View File

@@ -48,9 +48,6 @@
window.providersPromise = fetch("/auth/providers", { window.providersPromise = fetch("/auth/providers", {
credentials: "same-origin", credentials: "same-origin",
}); });
if (!window.globalThis) {
window.globalThis = window;
}
</script> </script>
<script> <script>

View File

@@ -71,9 +71,6 @@
import("<%= latestAppJS %>"); import("<%= latestAppJS %>");
window.customPanelJS = "<%= latestCustomPanelJS %>"; window.customPanelJS = "<%= latestCustomPanelJS %>";
window.latestJS = true; window.latestJS = true;
if (!window.globalThis) {
window.globalThis = window;
}
</script> </script>
<script> <script>
{% for extra_module in extra_modules -%} {% for extra_module in extra_modules -%}

View File

@@ -80,9 +80,6 @@
window.stepsPromise = fetch("/api/onboarding", { window.stepsPromise = fetch("/api/onboarding", {
credentials: "same-origin", credentials: "same-origin",
}); });
if (!window.globalThis) {
window.globalThis = window;
}
</script> </script>
<script> <script>

View File

@@ -229,7 +229,7 @@ class HassTabsSubpage extends LitElement {
color: var(--sidebar-text-color); color: var(--sidebar-text-color);
text-decoration: none; text-decoration: none;
} }
.bottom-bar a { :host([narrow]) .toolbar a {
width: 25%; width: 25%;
} }

View File

@@ -12,10 +12,7 @@ import { HASSDomEvent } from "../common/dom/fire_event";
import { extractSearchParamsObject } from "../common/url/search-params"; import { extractSearchParamsObject } from "../common/url/search-params";
import { subscribeOne } from "../common/util/subscribe-one"; import { subscribeOne } from "../common/util/subscribe-one";
import { AuthUrlSearchParams, hassUrl } from "../data/auth"; import { AuthUrlSearchParams, hassUrl } from "../data/auth";
import { import { fetchDiscoveryInformation } from "../data/discovery";
DiscoveryInformation,
fetchDiscoveryInformation,
} from "../data/discovery";
import { import {
fetchOnboardingOverview, fetchOnboardingOverview,
OnboardingResponses, OnboardingResponses,
@@ -71,8 +68,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
@state() private _steps?: OnboardingStep[]; @state() private _steps?: OnboardingStep[];
@state() private _discoveryInformation?: DiscoveryInformation;
protected render(): TemplateResult { protected render(): TemplateResult {
const step = this._curStep()!; const step = this._curStep()!;
@@ -92,7 +87,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
? html`<onboarding-restore-snapshot ? html`<onboarding-restore-snapshot
.localize=${this.localize} .localize=${this.localize}
.restoring=${this._restoring} .restoring=${this._restoring}
.discoveryInformation=${this._discoveryInformation}
@restoring=${this._restoringSnapshot} @restoring=${this._restoringSnapshot}
> >
</onboarding-restore-snapshot>` </onboarding-restore-snapshot>`

View File

@@ -5,11 +5,9 @@ import "@polymer/paper-radio-button/paper-radio-button";
import "@polymer/paper-radio-group/paper-radio-group"; import "@polymer/paper-radio-group/paper-radio-group";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize"; import type { LocalizeFunc } from "../common/translations/localize";
import "../components/map/ha-locations-editor"; import "../components/map/ha-location-editor";
import type { MarkerLocation } from "../components/map/ha-locations-editor";
import { createTimezoneListEl } from "../components/timezone-datalist"; import { createTimezoneListEl } from "../components/timezone-datalist";
import { import {
ConfigUpdateValues, ConfigUpdateValues,
@@ -83,14 +81,14 @@ class OnboardingCoreConfig extends LitElement {
</div> </div>
<div class="row"> <div class="row">
<ha-locations-editor <ha-location-editor
class="flex" class="flex"
.hass=${this.hass} .hass=${this.hass}
.locations=${this._markerLocation(this._locationValue)} .location=${this._locationValue}
zoom="14" .fitZoom=${14}
.darkMode=${mql.matches} .darkMode=${mql.matches}
@location-updated=${this._locationChanged} @change=${this._locationChanged}
></ha-locations-editor> ></ha-location-editor>
</div> </div>
<div class="row"> <div class="row">
@@ -210,24 +208,13 @@ class OnboardingCoreConfig extends LitElement {
return this._unitSystem !== undefined ? this._unitSystem : "metric"; return this._unitSystem !== undefined ? this._unitSystem : "metric";
} }
private _markerLocation = memoizeOne(
(location: [number, number]): MarkerLocation[] => [
{
id: "location",
latitude: location[0],
longitude: location[1],
location_editable: true,
},
]
);
private _handleChange(ev: PolymerChangedEvent<string>) { private _handleChange(ev: PolymerChangedEvent<string>) {
const target = ev.currentTarget as PaperInputElement; const target = ev.currentTarget as PaperInputElement;
this[`_${target.name}`] = target.value; this[`_${target.name}`] = target.value;
} }
private _locationChanged(ev) { private _locationChanged(ev) {
this._location = ev.detail.location; this._location = ev.currentTarget.location;
} }
private _unitSystemChanged( private _unitSystemChanged(

View File

@@ -4,12 +4,9 @@ import { customElement, property } from "lit/decorators";
import "../../hassio/src/components/hassio-ansi-to-html"; import "../../hassio/src/components/hassio-ansi-to-html";
import { showHassioSnapshotDialog } from "../../hassio/src/dialogs/snapshot/show-dialog-hassio-snapshot"; import { showHassioSnapshotDialog } from "../../hassio/src/dialogs/snapshot/show-dialog-hassio-snapshot";
import { showSnapshotUploadDialog } from "../../hassio/src/dialogs/snapshot/show-dialog-snapshot-upload"; import { showSnapshotUploadDialog } from "../../hassio/src/dialogs/snapshot/show-dialog-snapshot-upload";
import { navigate } from "../common/navigate";
import type { LocalizeFunc } from "../common/translations/localize"; import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-card"; import "../components/ha-card";
import {
DiscoveryInformation,
fetchDiscoveryInformation,
} from "../data/discovery";
import { makeDialogManager } from "../dialogs/make-dialog-manager"; import { makeDialogManager } from "../dialogs/make-dialog-manager";
import { ProvideHassLitMixin } from "../mixins/provide-hass-lit-mixin"; import { ProvideHassLitMixin } from "../mixins/provide-hass-lit-mixin";
import { haStyle } from "../resources/styles"; import { haStyle } from "../resources/styles";
@@ -29,9 +26,6 @@ class OnboardingRestoreSnapshot extends ProvideHassLitMixin(LitElement) {
@property({ type: Boolean }) public restoring = false; @property({ type: Boolean }) public restoring = false;
@property({ attribute: false })
public discoveryInformation?: DiscoveryInformation;
protected render(): TemplateResult { protected render(): TemplateResult {
return this.restoring return this.restoring
? html`<ha-card ? html`<ha-card
@@ -64,14 +58,13 @@ class OnboardingRestoreSnapshot extends ProvideHassLitMixin(LitElement) {
private async _checkRestoreStatus(): Promise<void> { private async _checkRestoreStatus(): Promise<void> {
if (this.restoring) { if (this.restoring) {
try { try {
const response = await fetchDiscoveryInformation(); const response = await fetch("/api/hassio/supervisor/info", {
method: "GET",
if ( });
!this.discoveryInformation || if (response.status === 401) {
this.discoveryInformation.uuid !== response.uuid // If we get a unauthorized response, the restore is done
) { navigate("/", { replace: true });
// When the UUID changes, the restore is complete location.reload();
window.location.replace("/");
} }
} catch (err) { } catch (err) {
// We fully expected issues with fetching info untill restore is complete. // We fully expected issues with fetching info untill restore is complete.
@@ -83,7 +76,6 @@ class OnboardingRestoreSnapshot extends ProvideHassLitMixin(LitElement) {
showHassioSnapshotDialog(this, { showHassioSnapshotDialog(this, {
slug, slug,
onboarding: true, onboarding: true,
localize: this.localize,
}); });
} }

View File

@@ -6,7 +6,7 @@ import { TagTrigger } from "../../../../../data/automation";
import { fetchTags, Tag } from "../../../../../data/tag"; import { fetchTags, Tag } from "../../../../../data/tag";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import { TriggerElement } from "../ha-automation-trigger-row"; import { TriggerElement } from "../ha-automation-trigger-row";
import "../../../../../components/ha-paper-dropdown-menu";
@customElement("ha-automation-trigger-tag") @customElement("ha-automation-trigger-tag")
export class HaTagTrigger extends LitElement implements TriggerElement { export class HaTagTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;

View File

@@ -8,8 +8,7 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { UNIT_C } from "../../../common/const"; import { UNIT_C } from "../../../common/const";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/map/ha-locations-editor"; import "../../../components/map/ha-location-editor";
import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
import { createTimezoneListEl } from "../../../components/timezone-datalist"; import { createTimezoneListEl } from "../../../components/timezone-datalist";
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core"; import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
import type { PolymerChangedEvent } from "../../../polymer-types"; import type { PolymerChangedEvent } from "../../../polymer-types";
@@ -21,13 +20,13 @@ class ConfigCoreForm extends LitElement {
@state() private _working = false; @state() private _working = false;
@state() private _location?: [number, number]; @state() private _location!: [number, number];
@state() private _elevation?: string; @state() private _elevation!: string;
@state() private _unitSystem?: ConfigUpdateValues["unit_system"]; @state() private _unitSystem!: ConfigUpdateValues["unit_system"];
@state() private _timeZone?: string; @state() private _timeZone!: string;
protected render(): TemplateResult { protected render(): TemplateResult {
const canEdit = ["storage", "default"].includes( const canEdit = ["storage", "default"].includes(
@@ -53,16 +52,16 @@ class ConfigCoreForm extends LitElement {
: ""} : ""}
<div class="row"> <div class="row">
<ha-locations-editor <ha-location-editor
class="flex" class="flex"
.hass=${this.hass} .hass=${this.hass}
.locations=${this._markerLocation( .location=${this._locationValue(
this._location,
this.hass.config.latitude, this.hass.config.latitude,
this.hass.config.longitude, this.hass.config.longitude
this._location
)} )}
@location-updated=${this._locationChanged} @change=${this._locationChanged}
></ha-locations-editor> ></ha-location-editor>
</div> </div>
<div class="row"> <div class="row">
@@ -163,19 +162,8 @@ class ConfigCoreForm extends LitElement {
input.inputElement.appendChild(createTimezoneListEl()); input.inputElement.appendChild(createTimezoneListEl());
} }
private _markerLocation = memoizeOne( private _locationValue = memoizeOne(
( (location, lat, lng) => location || [Number(lat), Number(lng)]
lat: number,
lng: number,
location?: [number, number]
): MarkerLocation[] => [
{
id: "location",
latitude: location ? location[0] : lat,
longitude: location ? location[1] : lng,
location_editable: true,
},
]
); );
private get _elevationValue() { private get _elevationValue() {
@@ -204,7 +192,7 @@ class ConfigCoreForm extends LitElement {
} }
private _locationChanged(ev) { private _locationChanged(ev) {
this._location = ev.detail.location; this._location = ev.currentTarget.location;
} }
private _unitSystemChanged( private _unitSystemChanged(
@@ -216,10 +204,11 @@ class ConfigCoreForm extends LitElement {
private async _save() { private async _save() {
this._working = true; this._working = true;
try { try {
const location = this._location || [ const location = this._locationValue(
this._location,
this.hass.config.latitude, this.hass.config.latitude,
this.hass.config.longitude, this.hass.config.longitude
]; );
await saveCoreConfig(this.hass, { await saveCoreConfig(this.hass, {
latitude: location[0], latitude: location[0],
longitude: location[1], longitude: location[1],

View File

@@ -9,14 +9,13 @@ import {
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-network";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-checkbox"; import "../../../components/ha-checkbox";
import "../../../components/ha-network";
import "../../../components/ha-settings-row"; import "../../../components/ha-settings-row";
import { fetchNetworkInfo } from "../../../data/hassio/network";
import { import {
getNetworkConfig,
NetworkConfig, NetworkConfig,
getNetworkConfig,
setNetworkConfig, setNetworkConfig,
} from "../../../data/network"; } from "../../../data/network";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
@@ -74,19 +73,7 @@ class ConfigNetwork extends LitElement {
private async _load() { private async _load() {
this._error = undefined; this._error = undefined;
try { try {
const coreNetwork = await getNetworkConfig(this.hass); this._networkConfig = await getNetworkConfig(this.hass);
if (isComponentLoaded(this.hass, "hassio")) {
const supervisorNetwork = await fetchNetworkInfo(this.hass);
const interfaces = new Set(
supervisorNetwork.interfaces.map((int) => int.interface)
);
if (interfaces.size) {
coreNetwork.adapters = coreNetwork.adapters.filter((adapter) =>
interfaces.has(adapter.name)
);
}
}
this._networkConfig = coreNetwork;
} catch (err) { } catch (err) {
this._error = err.message || err; this._error = err.message || err;
} }

View File

@@ -11,11 +11,7 @@ import { slugify } from "../../../common/string/slugify";
import "../../../components/entity/ha-battery-icon"; import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-icon-next"; import "../../../components/ha-icon-next";
import { AreaRegistryEntry } from "../../../data/area_registry"; import { AreaRegistryEntry } from "../../../data/area_registry";
import { import { ConfigEntry, disableConfigEntry } from "../../../data/config_entries";
ConfigEntry,
disableConfigEntry,
DisableConfigEntryResult,
} from "../../../data/config_entries";
import { import {
computeDeviceName, computeDeviceName,
DeviceRegistryEntry, DeviceRegistryEntry,
@@ -29,10 +25,7 @@ import {
} from "../../../data/entity_registry"; } from "../../../data/entity_registry";
import { SceneEntities, showSceneEditor } from "../../../data/scene"; import { SceneEntities, showSceneEditor } from "../../../data/scene";
import { findRelated, RelatedResult } from "../../../data/search"; import { findRelated, RelatedResult } from "../../../data/search";
import { import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-error-screen"; import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-tabs-subpage"; import "../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
@@ -678,41 +671,13 @@ export class HaConfigDevicePage extends LitElement {
dismissText: this.hass.localize("ui.common.no"), dismissText: this.hass.localize("ui.common.no"),
})) }))
) { ) {
let result: DisableConfigEntryResult; disableConfigEntry(this.hass, cnfg_entry);
try {
// eslint-disable-next-line no-await-in-loop
result = await disableConfigEntry(this.hass, cnfg_entry);
} catch (err) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_entry.disable_error"
),
text: err.message,
});
return;
}
if (result.require_restart) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.disable_restart_confirm"
),
});
}
delete updates.disabled_by; delete updates.disabled_by;
} }
} }
} }
} }
try { await updateDeviceRegistryEntry(this.hass, this.deviceId, updates);
await updateDeviceRegistryEntry(this.hass, this.deviceId, updates);
} catch (err) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.devices.update_device_error"
),
text: err.message,
});
}
if ( if (
!oldDeviceName || !oldDeviceName ||

View File

@@ -193,6 +193,9 @@ class HaInputNumberForm extends LitElement {
.form { .form {
color: var(--primary-text-color); color: var(--primary-text-color);
} }
ha-paper-dropdown-menu {
display: block;
}
`, `,
]; ];
} }

View File

@@ -196,6 +196,9 @@ class HaInputSelectForm extends LitElement {
mwc-button { mwc-button {
margin-left: 8px; margin-left: 8px;
} }
ha-paper-dropdown-menu {
display: block;
}
`, `,
]; ];
} }

View File

@@ -179,6 +179,9 @@ class HaInputTextForm extends LitElement {
.row { .row {
padding: 16px 0; padding: 16px 0;
} }
ha-paper-dropdown-menu {
display: block;
}
`, `,
]; ];
} }

View File

@@ -18,7 +18,6 @@ import {
ConfigEntry, ConfigEntry,
deleteConfigEntry, deleteConfigEntry,
disableConfigEntry, disableConfigEntry,
DisableConfigEntryResult,
enableConfigEntry, enableConfigEntry,
reloadConfigEntry, reloadConfigEntry,
updateConfigEntry, updateConfigEntry,
@@ -111,7 +110,6 @@ export class HaIntegrationCard extends LitElement {
: undefined} : undefined}
.localizedDomainName=${item ? item.localized_domain_name : undefined} .localizedDomainName=${item ? item.localized_domain_name : undefined}
.manifest=${this.manifest} .manifest=${this.manifest}
.configEntry=${item}
> >
${this.items.length > 1 ${this.items.length > 1
? html` ? html`
@@ -468,11 +466,6 @@ export class HaIntegrationCard extends LitElement {
private _showSystemOptions(configEntry: ConfigEntry) { private _showSystemOptions(configEntry: ConfigEntry) {
showConfigEntrySystemOptionsDialog(this, { showConfigEntrySystemOptionsDialog(this, {
entry: configEntry, entry: configEntry,
manifest: this.manifest,
entryUpdated: (entry) =>
fireEvent(this, "entry-updated", {
entry,
}),
}); });
} }
@@ -488,18 +481,7 @@ export class HaIntegrationCard extends LitElement {
if (!confirmed) { if (!confirmed) {
return; return;
} }
let result: DisableConfigEntryResult; const result = await disableConfigEntry(this.hass, entryId);
try {
result = await disableConfigEntry(this.hass, entryId);
} catch (err) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_entry.disable_error"
),
text: err.message,
});
return;
}
if (result.require_restart) { if (result.require_restart) {
showAlertDialog(this, { showAlertDialog(this, {
text: this.hass.localize( text: this.hass.localize(
@@ -515,18 +497,7 @@ export class HaIntegrationCard extends LitElement {
private async _enableIntegration(configEntry: ConfigEntry) { private async _enableIntegration(configEntry: ConfigEntry) {
const entryId = configEntry.entry_id; const entryId = configEntry.entry_id;
let result: DisableConfigEntryResult; const result = await enableConfigEntry(this.hass, entryId);
try {
result = await enableConfigEntry(this.hass, entryId);
} catch (err) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_entry.disable_error"
),
text: err.message,
});
return;
}
if (result.require_restart) { if (result.require_restart) {
showAlertDialog(this, { showAlertDialog(this, {
@@ -590,10 +561,10 @@ export class HaIntegrationCard extends LitElement {
if (newName === null) { if (newName === null) {
return; return;
} }
const result = await updateConfigEntry(this.hass, configEntry.entry_id, { const newEntry = await updateConfigEntry(this.hass, configEntry.entry_id, {
title: newName, title: newName,
}); });
fireEvent(this, "entry-updated", { entry: result.config_entry }); fireEvent(this, "entry-updated", { entry: newEntry });
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@@ -1,9 +1,8 @@
import { mdiCloud, mdiPackageVariant, mdiSyncOff } from "@mdi/js"; import { mdiCloud, mdiPackageVariant } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import { css, html, LitElement, TemplateResult } from "lit"; import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { ConfigEntry } from "../../../data/config_entries";
import { domainToName, IntegrationManifest } from "../../../data/integration"; import { domainToName, IntegrationManifest } from "../../../data/integration";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url"; import { brandsUrl } from "../../../util/brands-url";
@@ -22,8 +21,6 @@ export class HaIntegrationHeader extends LitElement {
@property({ attribute: false }) public manifest?: IntegrationManifest; @property({ attribute: false }) public manifest?: IntegrationManifest;
@property({ attribute: false }) public configEntry?: ConfigEntry;
protected render(): TemplateResult { protected render(): TemplateResult {
let primary: string; let primary: string;
let secondary: string | undefined; let secondary: string | undefined;
@@ -62,15 +59,6 @@ export class HaIntegrationHeader extends LitElement {
), ),
]); ]);
} }
if (this.configEntry?.pref_disable_polling) {
icons.push([
mdiSyncOff,
this.hass.localize(
"ui.panel.config.integrations.config_entry.disabled_polling"
),
]);
}
} }
return html` return html`

View File

@@ -9,12 +9,7 @@ import {
} from "../../../../../data/zha"; } from "../../../../../data/zha";
import "../../../../../layouts/hass-tabs-subpage"; import "../../../../../layouts/hass-tabs-subpage";
import type { HomeAssistant, Route } from "../../../../../types"; import type { HomeAssistant, Route } from "../../../../../types";
import { import { Network, Edge, Node, EdgeOptions } from "vis-network/peer";
Network,
Edge,
Node,
EdgeOptions,
} from "vis-network/peer/esm/vis-network";
import "../../../../../common/search/search-input"; import "../../../../../common/search/search-input";
import "../../../../../components/device/ha-device-picker"; import "../../../../../components/device/ha-device-picker";
import "../../../../../components/ha-button-menu"; import "../../../../../components/ha-button-menu";
@@ -26,7 +21,6 @@ import "../../../../../components/ha-checkbox";
import type { HaCheckbox } from "../../../../../components/ha-checkbox"; import type { HaCheckbox } from "../../../../../components/ha-checkbox";
import { zhaTabs } from "./zha-config-dashboard"; import { zhaTabs } from "./zha-config-dashboard";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import "../../../../../components/ha-formfield";
@customElement("zha-network-visualization-page") @customElement("zha-network-visualization-page")
export class ZHANetworkVisualizationPage extends LitElement { export class ZHANetworkVisualizationPage extends LitElement {
@@ -34,7 +28,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
@property({ attribute: false }) public route!: Route; @property({ attribute: false }) public route!: Route;
@property({ type: Boolean, reflect: true }) public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public isWide!: boolean; @property({ type: Boolean }) public isWide!: boolean;
@@ -73,6 +67,8 @@ export class ZHANetworkVisualizationPage extends LitElement {
{}, {},
{ {
autoResize: true, autoResize: true,
height: window.innerHeight + "px",
width: window.innerWidth + "px",
layout: { layout: {
improvedLayout: true, improvedLayout: true,
}, },
@@ -139,35 +135,17 @@ export class ZHANetworkVisualizationPage extends LitElement {
"ui.panel.config.zha.visualization.header" "ui.panel.config.zha.visualization.header"
)} )}
> >
${this.narrow <div class="table-header">
? html` <search-input
<div slot="header"> no-label-float
<search-input no-underline
no-label-float @value-changed=${this._handleSearchChange}
no-underline .filter=${this._filter}
class="header" .label=${this.hass.localize(
@value-changed=${this._handleSearchChange} "ui.panel.config.zha.visualization.highlight_label"
.filter=${this._filter} )}
.label=${this.hass.localize( >
"ui.panel.config.zha.visualization.highlight_label" </search-input>
)}
>
</search-input>
</div>
`
: ""}
<div class="header">
${!this.narrow
? html`<search-input
no-label-float
no-underline
@value-changed=${this._handleSearchChange}
.filter=${this._filter}
.label=${this.hass.localize(
"ui.panel.config.zha.visualization.highlight_label"
)}
></search-input>`
: ""}
<ha-device-picker <ha-device-picker
.hass=${this.hass} .hass=${this.hass}
.value=${this.zoomedDeviceId} .value=${this.zoomedDeviceId}
@@ -177,24 +155,16 @@ export class ZHANetworkVisualizationPage extends LitElement {
.deviceFilter=${(device) => this._filterDevices(device)} .deviceFilter=${(device) => this._filterDevices(device)}
@value-changed=${this._onZoomToDevice} @value-changed=${this._onZoomToDevice}
></ha-device-picker> ></ha-device-picker>
<div class="controls"> <ha-checkbox
<ha-formfield @change=${this._handleCheckboxChange}
.label=${this.hass!.localize( .checked=${this._autoZoom}
"ui.panel.config.zha.visualization.auto_zoom" ></ha-checkbox
)} >${this.hass!.localize("ui.panel.config.zha.visualization.auto_zoom")}
> <mwc-button @click=${this._refreshTopology}
<ha-checkbox >${this.hass!.localize(
@change=${this._handleCheckboxChange} "ui.panel.config.zha.visualization.refresh_topology"
.checked=${this._autoZoom} )}</mwc-button
> >
</ha-checkbox>
</ha-formfield>
<mwc-button @click=${this._refreshTopology}>
${this.hass!.localize(
"ui.panel.config.zha.visualization.refresh_topology"
)}
</mwc-button>
</div>
</div> </div>
<div id="visualization"></div> <div id="visualization"></div>
</hass-tabs-subpage> </hass-tabs-subpage>
@@ -382,23 +352,30 @@ export class ZHANetworkVisualizationPage extends LitElement {
return [ return [
css` css`
.header { .header {
border-bottom: 1px solid var(--divider-color); font-family: var(--paper-font-display1_-_font-family);
padding: 0 8px; -webkit-font-smoothing: var(
--paper-font-display1_-_-webkit-font-smoothing
);
font-size: var(--paper-font-display1_-_font-size);
font-weight: var(--paper-font-display1_-_font-weight);
letter-spacing: var(--paper-font-display1_-_letter-spacing);
line-height: var(--paper-font-display1_-_line-height);
opacity: var(--dark-primary-opacity);
}
.table-header {
border-bottom: 1px solid --divider-color;
padding: 0 16px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; flex-direction: row;
height: var(--header-height); height: var(--header-height);
box-sizing: border-box;
} }
.header > * { :host([narrow]) .table-header {
padding: 0 8px;
}
:host([narrow]) .header {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
height: var(--header-height) * 2; height: var(--header-height) * 3;
} }
.search-toolbar { .search-toolbar {
@@ -409,34 +386,34 @@ export class ZHANetworkVisualizationPage extends LitElement {
} }
search-input { search-input {
position: relative;
top: 2px;
flex: 1; flex: 1;
} }
:host(:not([narrow])) search-input {
margin: 5px;
}
search-input.header { search-input.header {
display: block; left: -8px;
position: relative;
top: -2px;
color: var(--secondary-text-color);
} }
ha-device-picker { ha-device-picker {
flex: 1; flex: 1;
position: relative;
top: -4px;
} }
.controls { :host(:not([narrow])) ha-device-picker {
display: flex; margin: 5px;
align-items: center;
justify-content: space-between;
} }
#visualization { mwc-button {
height: calc(100% - var(--header-height)); font-weight: 500;
width: 100%; color: var(--primary-color);
} }
:host([narrow]) #visualization {
height: calc(100% - (var(--header-height) * 2)); :host(:not([narrow])) mwc-button {
margin: 5px;
} }
`, `,
]; ];

View File

@@ -24,7 +24,6 @@ import {
import "../../../../../layouts/hass-tabs-subpage"; import "../../../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types"; import type { HomeAssistant, Route } from "../../../../../types";
import { fileDownload } from "../../../../../util/file_download";
import "../../../ha-config-section"; import "../../../ha-config-section";
import { showZWaveJSAddNodeDialog } from "./show-dialog-zwave_js-add-node"; import { showZWaveJSAddNodeDialog } from "./show-dialog-zwave_js-add-node";
import { showZWaveJSRemoveNodeDialog } from "./show-dialog-zwave_js-remove-node"; import { showZWaveJSRemoveNodeDialog } from "./show-dialog-zwave_js-remove-node";
@@ -313,7 +312,12 @@ class ZWaveJSConfigDashboard extends LitElement {
return; return;
} }
fileDownload(this, signedPath.path, `zwave_js_dump.jsonl`); const a = document.createElement("a");
a.href = signedPath.path;
a.download = `zwave_js_dump.jsonl`;
this.shadowRoot!.appendChild(a);
a.click();
this.shadowRoot!.removeChild(a);
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@@ -9,9 +9,13 @@ import { computeRTLDirection } from "../../../common/util/compute_rtl";
import { createCloseHeading } from "../../../components/ha-dialog"; import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-formfield"; import "../../../components/ha-formfield";
import "../../../components/ha-switch"; import "../../../components/ha-switch";
import "../../../components/map/ha-locations-editor"; import "../../../components/map/ha-location-editor";
import type { MarkerLocation } from "../../../components/map/ha-locations-editor"; import {
import { getZoneEditorInitData, ZoneMutableParams } from "../../../data/zone"; defaultRadiusColor,
getZoneEditorInitData,
passiveRadiusColor,
ZoneMutableParams,
} from "../../../data/zone";
import { haStyleDialog } from "../../../resources/styles"; import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { ZoneDetailDialogParams } from "./show-dialog-zone-detail"; import { ZoneDetailDialogParams } from "./show-dialog-zone-detail";
@@ -128,19 +132,17 @@ class DialogZoneDetail extends LitElement {
)}" )}"
.invalid=${iconValid} .invalid=${iconValid}
></paper-input> ></paper-input>
<ha-locations-editor <ha-location-editor
class="flex" class="flex"
.hass=${this.hass} .hass=${this.hass}
.locations=${this._location( .location=${this._locationValue(this._latitude, this._longitude)}
this._latitude, .radius=${this._radius}
this._longitude, .radiusColor=${this._passive
this._radius, ? passiveRadiusColor
this._passive, : defaultRadiusColor}
this._icon .icon=${this._icon}
)} @change=${this._locationChanged}
@location-updated=${this._locationChanged} ></ha-location-editor>
@radius-updated=${this._radiusChanged}
></ha-locations-editor>
<div class="location"> <div class="location">
<paper-input <paper-input
.value=${this._latitude} .value=${this._latitude}
@@ -220,40 +222,11 @@ class DialogZoneDetail extends LitElement {
`; `;
} }
private _location = memoizeOne( private _locationValue = memoizeOne((lat, lng) => [Number(lat), Number(lng)]);
(
lat: number,
lng: number,
radius: number,
passive: boolean,
icon: string
): MarkerLocation[] => {
const computedStyles = getComputedStyle(this);
const zoneRadiusColor = computedStyles.getPropertyValue("--accent-color");
const passiveRadiusColor = computedStyles.getPropertyValue(
"--secondary-text-color"
);
return [
{
id: "location",
latitude: Number(lat),
longitude: Number(lng),
radius,
radius_color: passive ? passiveRadiusColor : zoneRadiusColor,
icon,
location_editable: true,
radius_editable: true,
},
];
}
);
private _locationChanged(ev: CustomEvent) { private _locationChanged(ev) {
[this._latitude, this._longitude] = ev.detail.location; [this._latitude, this._longitude] = ev.currentTarget.location;
} this._radius = ev.currentTarget.radius;
private _radiusChanged(ev: CustomEvent) {
this._radius = ev.detail.radius;
} }
private _passiveChanged(ev) { private _passiveChanged(ev) {
@@ -319,7 +292,7 @@ class DialogZoneDetail extends LitElement {
.location > *:last-child { .location > *:last-child {
margin-left: 4px; margin-left: 4px;
} }
ha-locations-editor { ha-location-editor {
margin-top: 16px; margin-top: 16px;
} }
a { a {

View File

@@ -31,8 +31,11 @@ import { saveCoreConfig } from "../../../data/core";
import { subscribeEntityRegistry } from "../../../data/entity_registry"; import { subscribeEntityRegistry } from "../../../data/entity_registry";
import { import {
createZone, createZone,
defaultRadiusColor,
deleteZone, deleteZone,
fetchZones, fetchZones,
homeRadiusColor,
passiveRadiusColor,
updateZone, updateZone,
Zone, Zone,
ZoneMutableParams, ZoneMutableParams,
@@ -70,15 +73,6 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
private _getZones = memoizeOne( private _getZones = memoizeOne(
(storageItems: Zone[], stateItems: HassEntity[]): MarkerLocation[] => { (storageItems: Zone[], stateItems: HassEntity[]): MarkerLocation[] => {
const computedStyles = getComputedStyle(this);
const zoneRadiusColor = computedStyles.getPropertyValue("--accent-color");
const passiveRadiusColor = computedStyles.getPropertyValue(
"--secondary-text-color"
);
const homeRadiusColor = computedStyles.getPropertyValue(
"--primary-color"
);
const stateLocations: MarkerLocation[] = stateItems.map( const stateLocations: MarkerLocation[] = stateItems.map(
(entityState) => ({ (entityState) => ({
id: entityState.entity_id, id: entityState.entity_id,
@@ -92,7 +86,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
? homeRadiusColor ? homeRadiusColor
: entityState.attributes.passive : entityState.attributes.passive
? passiveRadiusColor ? passiveRadiusColor
: zoneRadiusColor, : defaultRadiusColor,
location_editable: location_editable:
entityState.entity_id === "zone.home" && this._canEditCore, entityState.entity_id === "zone.home" && this._canEditCore,
radius_editable: false, radius_editable: false,
@@ -100,7 +94,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
); );
const storageLocations: MarkerLocation[] = storageItems.map((zone) => ({ const storageLocations: MarkerLocation[] = storageItems.map((zone) => ({
...zone, ...zone,
radius_color: zone.passive ? passiveRadiusColor : zoneRadiusColor, radius_color: zone.passive ? passiveRadiusColor : defaultRadiusColor,
location_editable: true, location_editable: true,
radius_editable: true, radius_editable: true,
})); }));
@@ -280,7 +274,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
} }
} }
public willUpdate(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
super.updated(changedProps); super.updated(changedProps);
const oldHass = changedProps.get("hass") as HomeAssistant | undefined; const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (oldHass && this._stateItems) { if (oldHass && this._stateItems) {
@@ -416,9 +410,8 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
if (this.narrow) { if (this.narrow) {
return; return;
} }
this._activeEntry = created.id;
await this.updateComplete; await this.updateComplete;
await this._map?.updateComplete; this._activeEntry = created.id;
this._map?.fitMarker(created.id); this._map?.fitMarker(created.id);
} }
@@ -434,9 +427,8 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
if (this.narrow || !fitMap) { if (this.narrow || !fitMap) {
return; return;
} }
this._activeEntry = entry.id;
await this.updateComplete; await this.updateComplete;
await this._map?.updateComplete; this._activeEntry = entry.id;
this._map?.fitMarker(entry.id); this._map?.fitMarker(entry.id);
} }

View File

@@ -11,8 +11,8 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import { dump, load } from "js-yaml"; import { dump, load } from "js-yaml";
import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time"; import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time";
import { isPatternInWord } from "../../../common/string/filter/filter";
import { computeRTL } from "../../../common/util/compute_rtl"; import { computeRTL } from "../../../common/util/compute_rtl";
import { escapeRegExp } from "../../../common/string/escape_regexp";
import { copyToClipboard } from "../../../common/util/copy-clipboard"; import { copyToClipboard } from "../../../common/util/copy-clipboard";
import "../../../components/entity/ha-entity-picker"; import "../../../components/entity/ha-entity-picker";
import "../../../components/ha-code-editor"; import "../../../components/ha-code-editor";
@@ -412,68 +412,72 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
} }
computeEntities(hass, _entityFilter, _stateFilter, _attributeFilter) { computeEntities(hass, _entityFilter, _stateFilter, _attributeFilter) {
const entityFilterRegExp = const _entityFilterLength = _entityFilter && _entityFilter.length;
_entityFilter && const _entityFilterLow = _entityFilter && _entityFilter.toLowerCase();
RegExp(escapeRegExp(_entityFilter).replace(/\\\*/g, ".*"), "i");
const stateFilterRegExp = return Object.keys(hass.states)
_stateFilter && .map((key) => hass.states[key])
RegExp(escapeRegExp(_stateFilter).replace(/\\\*/g, ".*"), "i");
let keyFilterRegExp;
let valueFilterRegExp;
let multiMode = false;
if (_attributeFilter) {
const colonIndex = _attributeFilter.indexOf(":");
multiMode = colonIndex !== -1;
const keyFilter = multiMode
? _attributeFilter.substring(0, colonIndex).trim()
: _attributeFilter;
const valueFilter = multiMode
? _attributeFilter.substring(colonIndex + 1).trim()
: _attributeFilter;
keyFilterRegExp = RegExp(
escapeRegExp(keyFilter).replace(/\\\*/g, ".*"),
"i"
);
valueFilterRegExp = multiMode
? RegExp(escapeRegExp(valueFilter).replace(/\\\*/g, ".*"), "i")
: keyFilterRegExp;
}
return Object.values(hass.states)
.filter((value) => { .filter((value) => {
if ( if (
entityFilterRegExp && _entityFilter &&
!entityFilterRegExp.test(value.entity_id) && !isPatternInWord(
_entityFilterLow,
0,
_entityFilterLength,
value.entity_id.toLowerCase(),
0,
value.entity_id.length,
true
) &&
(value.attributes.friendly_name === undefined || (value.attributes.friendly_name === undefined ||
!entityFilterRegExp.test(value.attributes.friendly_name)) !isPatternInWord(
_entityFilterLow,
0,
_entityFilterLength,
value.attributes.friendly_name.toLowerCase(),
0,
value.attributes.friendly_name.length,
true
))
) { ) {
return false; return false;
} }
if (stateFilterRegExp && !stateFilterRegExp.test(value.state)) { if (!value.state.toLowerCase().includes(_stateFilter.toLowerCase())) {
return false; return false;
} }
if (keyFilterRegExp && valueFilterRegExp) { if (_attributeFilter !== "") {
for (const [key, attributeValue] of Object.entries( const attributeFilter = _attributeFilter.toLowerCase();
value.attributes const colonIndex = attributeFilter.indexOf(":");
)) { const multiMode = colonIndex !== -1;
const match = keyFilterRegExp.test(key);
if (match && !multiMode) { let keyFilter = attributeFilter;
let valueFilter = attributeFilter;
if (multiMode) {
// we need to filter keys and values separately
keyFilter = attributeFilter.substring(0, colonIndex).trim();
valueFilter = attributeFilter.substring(colonIndex + 1).trim();
}
const attributeKeys = Object.keys(value.attributes);
for (let i = 0; i < attributeKeys.length; i++) {
const key = attributeKeys[i];
if (key.includes(keyFilter) && !multiMode) {
return true; // in single mode we're already satisfied with this match return true; // in single mode we're already satisfied with this match
} }
if (!match && multiMode) { if (!key.includes(keyFilter) && multiMode) {
continue; continue;
} }
const attributeValue = value.attributes[key];
if ( if (
attributeValue !== undefined && attributeValue !== undefined &&
valueFilterRegExp.test(JSON.stringify(attributeValue)) JSON.stringify(attributeValue).toLowerCase().includes(valueFilter)
) { ) {
return true; return true;
} }

View File

@@ -1,4 +1,4 @@
import { Layout1d, scroll } from "../../resources/lit-virtualizer"; import { Layout1d, scroll } from "@lit-labs/virtualizer";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -100,6 +100,7 @@ class HaLogbook extends LitElement {
? scroll({ ? scroll({
items: this.entries, items: this.entries,
layout: Layout1d, layout: Layout1d,
// @ts-expect-error
renderItem: (item: LogbookEntry, index) => renderItem: (item: LogbookEntry, index) =>
this._renderLogbookItem(item, index), this._renderLogbookItem(item, index),
}) })
@@ -353,7 +354,15 @@ class HaLogbook extends LitElement {
} }
:host([virtualize]) .container { :host([virtualize]) .container {
display: block;
position: relative;
contain: strict;
height: 100%; height: 100%;
overflow: auto;
}
.container > * {
box-sizing: border-box;
} }
.narrow .entry { .narrow .entry {

View File

@@ -1,22 +1,22 @@
import { mdiRefresh } from "@mdi/js"; import { mdiRefresh } from "@mdi/js";
import "@material/mwc-icon-button";
import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
import { css, html, LitElement, PropertyValues } from "lit"; import { css, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeRTL } from "../../common/util/compute_rtl"; import { computeRTL } from "../../common/util/compute_rtl";
import "../../components/entity/ha-entity-picker"; import "../../components/entity/ha-entity-picker";
import "../../components/ha-circular-progress"; import "../../components/ha-circular-progress";
import "../../components/ha-date-range-picker"; import "../../components/ha-date-range-picker";
import type { DateRangePickerRanges } from "../../components/ha-date-range-picker"; import type { DateRangePickerRanges } from "../../components/ha-date-range-picker";
import "../../components/ha-icon-button";
import "../../components/ha-menu-button"; import "../../components/ha-menu-button";
import { import {
clearLogbookCache, clearLogbookCache,
getLogbookData, getLogbookData,
LogbookEntry, LogbookEntry,
} from "../../data/logbook"; } from "../../data/logbook";
import { fetchPersons } from "../../data/person";
import { loadTraceContexts, TraceContexts } from "../../data/trace"; import { loadTraceContexts, TraceContexts } from "../../data/trace";
import { fetchUsers } from "../../data/user"; import { fetchUsers } from "../../data/user";
import "../../layouts/ha-app-layout"; import "../../layouts/ha-app-layout";
@@ -44,7 +44,7 @@ export class HaPanelLogbook extends LitElement {
@state() private _ranges?: DateRangePickerRanges; @state() private _ranges?: DateRangePickerRanges;
private _fetchUserPromise?: Promise<void>; private _fetchUserDone?: Promise<unknown>;
@state() private _userIdToName = {}; @state() private _userIdToName = {};
@@ -136,7 +136,7 @@ export class HaPanelLogbook extends LitElement {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this.hass.loadBackendTranslation("title"); this.hass.loadBackendTranslation("title");
this._fetchUserPromise = this._fetchUserNames(); this._fetchUserDone = this._fetchUserNames();
const today = new Date(); const today = new Date();
today.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0);
@@ -198,19 +198,23 @@ export class HaPanelLogbook extends LitElement {
private async _fetchUserNames() { private async _fetchUserNames() {
const userIdToName = {}; const userIdToName = {};
// Start loading users // Start loading all the data
const userProm = this.hass.user?.is_admin && fetchUsers(this.hass); const personProm = fetchPersons(this.hass);
const userProm = this.hass.user!.is_admin && fetchUsers(this.hass);
// Process persons // Process persons
Object.values(this.hass.states).forEach((entity) => { const persons = await personProm;
if (
entity.attributes.user_id && for (const person of persons.storage) {
computeStateDomain(entity) === "person" if (person.user_id) {
) { userIdToName[person.user_id] = person.name;
this._userIdToName[entity.attributes.user_id] =
entity.attributes.friendly_name;
} }
}); }
for (const person of persons.config) {
if (person.user_id) {
userIdToName[person.user_id] = person.name;
}
}
// Process users // Process users
if (userProm) { if (userProm) {
@@ -258,7 +262,7 @@ export class HaPanelLogbook extends LitElement {
this._entityId this._entityId
), ),
isComponentLoaded(this.hass, "trace") ? loadTraceContexts(this.hass) : {}, isComponentLoaded(this.hass, "trace") ? loadTraceContexts(this.hass) : {},
this._fetchUserPromise, this._fetchUserDone,
]); ]);
this._entries = entries; this._entries = entries;

View File

@@ -9,12 +9,11 @@ import {
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { throttle } from "../../../common/util/throttle"; import { throttle } from "../../../common/util/throttle";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-circular-progress"; import "../../../components/ha-circular-progress";
import { fetchUsers } from "../../../data/user";
import { getLogbookData, LogbookEntry } from "../../../data/logbook"; import { getLogbookData, LogbookEntry } from "../../../data/logbook";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import "../../logbook/ha-logbook"; import "../../logbook/ha-logbook";
@@ -52,20 +51,18 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
}; };
} }
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: LogbookCardConfig; @state() private _config?: LogbookCardConfig;
@state() private _logbookEntries?: LogbookEntry[]; @state() private _logbookEntries?: LogbookEntry[];
@state() private _persons = {};
@state() private _configEntities?: EntityConfig[]; @state() private _configEntities?: EntityConfig[];
@state() private _userIdToName = {};
private _lastLogbookDate?: Date; private _lastLogbookDate?: Date;
private _fetchUserPromise?: Promise<void>;
private _throttleGetLogbookEntries = throttle(() => { private _throttleGetLogbookEntries = throttle(() => {
this._getLogBookData(); this._getLogBookData();
}, 10000); }, 10000);
@@ -117,7 +114,7 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
} }
protected firstUpdated(): void { protected firstUpdated(): void {
this._fetchUserPromise = this._fetchUserNames(); this._fetchPersonNames();
} }
protected updated(changedProperties: PropertyValues) { protected updated(changedProperties: PropertyValues) {
@@ -202,7 +199,7 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
virtualize virtualize
.hass=${this.hass} .hass=${this.hass}
.entries=${this._logbookEntries} .entries=${this._logbookEntries}
.userIdToName=${this._userIdToName} .userIdToName=${this._persons}
></ha-logbook> ></ha-logbook>
` `
: html` : html`
@@ -232,16 +229,13 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
const lastDate = this._lastLogbookDate || hoursToShowDate; const lastDate = this._lastLogbookDate || hoursToShowDate;
const now = new Date(); const now = new Date();
const [newEntries] = await Promise.all([ const newEntries = await getLogbookData(
getLogbookData( this.hass,
this.hass, lastDate.toISOString(),
lastDate.toISOString(), now.toISOString(),
now.toISOString(), this._configEntities!.map((entity) => entity.entity).toString(),
this._configEntities!.map((entity) => entity.entity).toString(), true
true );
),
this._fetchUserPromise,
]);
const logbookEntries = this._logbookEntries const logbookEntries = this._logbookEntries
? [...newEntries, ...this._logbookEntries] ? [...newEntries, ...this._logbookEntries]
@@ -254,34 +248,20 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
this._lastLogbookDate = now; this._lastLogbookDate = now;
} }
private async _fetchUserNames() { private _fetchPersonNames() {
const userIdToName = {}; if (!this.hass) {
return;
}
// Start loading users
const userProm = this.hass.user?.is_admin && fetchUsers(this.hass);
// Process persons
Object.values(this.hass!.states).forEach((entity) => { Object.values(this.hass!.states).forEach((entity) => {
if ( if (
entity.attributes.user_id && entity.attributes.user_id &&
computeStateDomain(entity) === "person" computeStateDomain(entity) === "person"
) { ) {
this._userIdToName[entity.attributes.user_id] = this._persons[entity.attributes.user_id] =
entity.attributes.friendly_name; entity.attributes.friendly_name;
} }
}); });
// Process users
if (userProm) {
const users = await userProm;
for (const user of users) {
if (!(user.id in userIdToName)) {
userIdToName[user.id] = user.name;
}
}
}
this._userIdToName = userIdToName;
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@@ -1,5 +1,14 @@
import { HassEntities, HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { LatLngTuple } from "leaflet"; import {
Circle,
CircleMarker,
LatLngTuple,
Layer,
Map,
Marker,
Polyline,
TileLayer,
} from "leaflet";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -8,106 +17,32 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import {
LeafletModuleType,
replaceTileLayer,
setupLeafletMap,
} from "../../../common/dom/setup-leaflet-map";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { debounce } from "../../../common/util/debounce";
import parseAspectRatio from "../../../common/util/parse-aspect-ratio"; import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import { fetchRecent } from "../../../data/history"; import { fetchRecent } from "../../../data/history";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import "../../../components/map/ha-entity-marker"; import "../../map/ha-entity-marker";
import { findEntities } from "../common/find-entities"; import { findEntities } from "../common/find-entities";
import { installResizeObserver } from "../common/install-resize-observer";
import { processConfigEntities } from "../common/process-config-entities"; import { processConfigEntities } from "../common/process-config-entities";
import { EntityConfig } from "../entity-rows/types"; import { EntityConfig } from "../entity-rows/types";
import { LovelaceCard } from "../types"; import { LovelaceCard } from "../types";
import { MapCardConfig } from "./types"; import { MapCardConfig } from "./types";
import "../../../components/map/ha-map";
import { mdiImageFilterCenterFocus } from "@mdi/js";
import type { HaMap, HaMapPaths } from "../../../components/map/ha-map";
import memoizeOne from "memoize-one";
const MINUTE = 60000;
const COLORS = [
"#0288D1",
"#00AA00",
"#984ea3",
"#00d2d5",
"#ff7f00",
"#af8d00",
"#7f80cd",
"#b3e900",
"#c42e60",
"#a65628",
"#f781bf",
"#8dd3c7",
];
@customElement("hui-map-card") @customElement("hui-map-card")
class HuiMapCard extends LitElement implements LovelaceCard { class HuiMapCard extends LitElement implements LovelaceCard {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true })
public isPanel = false;
@state()
private _history?: HassEntity[][];
@state()
private _config?: MapCardConfig;
@query("ha-map")
private _map?: HaMap;
private _date?: Date;
private _configEntities?: string[];
private _colorDict: Record<string, string> = {};
private _colorIndex = 0;
public setConfig(config: MapCardConfig): void {
if (!config) {
throw new Error("Error in card configuration.");
}
if (!config.entities?.length && !config.geo_location_sources) {
throw new Error(
"Either entities or geo_location_sources must be specified"
);
}
if (config.entities && !Array.isArray(config.entities)) {
throw new Error("Entities need to be an array");
}
if (
config.geo_location_sources &&
!Array.isArray(config.geo_location_sources)
) {
throw new Error("Geo_location_sources needs to be an array");
}
this._config = config;
this._configEntities = (config.entities
? processConfigEntities<EntityConfig>(config.entities)
: []
).map((entity) => entity.entity);
this._cleanupHistory();
}
public getCardSize(): number {
if (!this._config?.aspect_ratio) {
return 7;
}
const ratio = parseAspectRatio(this._config.aspect_ratio);
const ar =
ratio && ratio.w > 0 && ratio.h > 0
? `${((100 * ratio.h) / ratio.w).toFixed(2)}`
: "100";
return 1 + Math.floor(Number(ar) / 25) || 3;
}
public static async getConfigElement() { public static async getConfigElement() {
await import("../editor/config-elements/hui-map-card-editor"); await import("../editor/config-elements/hui-map-card-editor");
return document.createElement("hui-map-card-editor"); return document.createElement("hui-map-card-editor");
@@ -131,6 +66,129 @@ class HuiMapCard extends LitElement implements LovelaceCard {
return { type: "map", entities: foundEntities }; return { type: "map", entities: foundEntities };
} }
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true })
public isPanel = false;
@property()
private _history?: HassEntity[][];
private _date?: Date;
@property()
private _config?: MapCardConfig;
private _configEntities?: EntityConfig[];
// eslint-disable-next-line
private Leaflet?: LeafletModuleType;
private _leafletMap?: Map;
private _tileLayer?: TileLayer;
private _resizeObserver?: ResizeObserver;
private _debouncedResizeListener = debounce(
() => {
if (!this.isConnected || !this._leafletMap) {
return;
}
this._leafletMap.invalidateSize();
},
250,
false
);
private _mapItems: Array<Marker | Circle> = [];
private _mapZones: Array<Marker | Circle> = [];
private _mapPaths: Array<Polyline | CircleMarker> = [];
private _colorDict: Record<string, string> = {};
private _colorIndex = 0;
private _colors: string[] = [
"#0288D1",
"#00AA00",
"#984ea3",
"#00d2d5",
"#ff7f00",
"#af8d00",
"#7f80cd",
"#b3e900",
"#c42e60",
"#a65628",
"#f781bf",
"#8dd3c7",
];
public setConfig(config: MapCardConfig): void {
if (!config) {
throw new Error("Error in card configuration.");
}
if (!config.entities?.length && !config.geo_location_sources) {
throw new Error(
"Either entities or geo_location_sources must be specified"
);
}
if (config.entities && !Array.isArray(config.entities)) {
throw new Error("Entities need to be an array");
}
if (
config.geo_location_sources &&
!Array.isArray(config.geo_location_sources)
) {
throw new Error("Geo_location_sources needs to be an array");
}
this._config = config;
this._configEntities = config.entities
? processConfigEntities(config.entities)
: [];
this._cleanupHistory();
}
public getCardSize(): number {
if (!this._config?.aspect_ratio) {
return 7;
}
const ratio = parseAspectRatio(this._config.aspect_ratio);
const ar =
ratio && ratio.w > 0 && ratio.h > 0
? `${((100 * ratio.h) / ratio.w).toFixed(2)}`
: "100";
return 1 + Math.floor(Number(ar) / 25) || 3;
}
public connectedCallback(): void {
super.connectedCallback();
this._attachObserver();
if (this.hasUpdated) {
this.loadMap();
}
}
public disconnectedCallback(): void {
super.disconnectedCallback();
if (this._leafletMap) {
this._leafletMap.remove();
this._leafletMap = undefined;
this.Leaflet = undefined;
}
if (this._resizeObserver) {
this._resizeObserver.unobserve(this._mapEl);
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._config) { if (!this._config) {
return html``; return html``;
@@ -138,29 +196,22 @@ class HuiMapCard extends LitElement implements LovelaceCard {
return html` return html`
<ha-card id="card" .header=${this._config.title}> <ha-card id="card" .header=${this._config.title}>
<div id="root"> <div id="root">
<ha-map <div
.hass=${this.hass} id="map"
.entities=${this._getEntities( class=${classMap({ dark: this._config.dark_mode === true })}
this.hass.states, ></div>
this._config, <ha-icon-button
this._configEntities
)}
.paths=${this._getHistoryPaths(this._config, this._history)}
.darkMode=${this._config.dark_mode}
></ha-map>
<mwc-icon-button
@click=${this._fitMap} @click=${this._fitMap}
tabindex="0" tabindex="0"
icon="hass:image-filter-center-focus"
title="Reset focus" title="Reset focus"
> ></ha-icon-button>
<ha-svg-icon .path=${mdiImageFilterCenterFocus}></ha-svg-icon>
</mwc-icon-button>
</div> </div>
</ha-card> </ha-card>
`; `;
} }
protected shouldUpdate(changedProps: PropertyValues) { protected shouldUpdate(changedProps) {
if (!changedProps.has("hass") || changedProps.size > 1) { if (!changedProps.has("hass") || changedProps.size > 1) {
return true; return true;
} }
@@ -177,7 +228,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
// Check if any state has changed // Check if any state has changed
for (const entity of this._configEntities) { for (const entity of this._configEntities) {
if (oldHass.states[entity] !== this.hass!.states[entity]) { if (oldHass.states[entity.entity] !== this.hass!.states[entity.entity]) {
return true; return true;
} }
} }
@@ -187,12 +238,17 @@ class HuiMapCard extends LitElement implements LovelaceCard {
protected firstUpdated(changedProps: PropertyValues): void { protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
if (this.isConnected) {
this.loadMap();
}
const root = this.shadowRoot!.getElementById("root"); const root = this.shadowRoot!.getElementById("root");
if (!this._config || this.isPanel || !root) { if (!this._config || this.isPanel || !root) {
return; return;
} }
this._attachObserver();
if (!this._config.aspect_ratio) { if (!this._config.aspect_ratio) {
root.style.paddingBottom = "100%"; root.style.paddingBottom = "100%";
return; return;
@@ -207,86 +263,172 @@ class HuiMapCard extends LitElement implements LovelaceCard {
} }
protected updated(changedProps: PropertyValues): void { protected updated(changedProps: PropertyValues): void {
if (changedProps.has("hass") || changedProps.has("_history")) {
this._drawEntities();
this._fitMap();
}
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (oldHass && oldHass.themes.darkMode !== this.hass.themes.darkMode) {
this._replaceTileLayer();
}
}
if (
changedProps.has("_config") &&
changedProps.get("_config") !== undefined
) {
this.updateMap(changedProps.get("_config") as MapCardConfig);
}
if (this._config?.hours_to_show && this._configEntities?.length) { if (this._config?.hours_to_show && this._configEntities?.length) {
const minute = 60000;
if (changedProps.has("_config")) { if (changedProps.has("_config")) {
this._getHistory(); this._getHistory();
} else if (Date.now() - this._date!.getTime() >= MINUTE) { } else if (Date.now() - this._date!.getTime() >= minute) {
this._getHistory(); this._getHistory();
} }
} }
} }
private _fitMap() { private get _mapEl(): HTMLDivElement {
this._map?.fitMap(); return this.shadowRoot!.getElementById("map") as HTMLDivElement;
} }
private _getColor(entityId: string): string { private async loadMap(): Promise<void> {
let color = this._colorDict[entityId]; [this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
if (color) { this._mapEl,
return color; this._config!.dark_mode ?? this.hass.themes.darkMode
);
this._drawEntities();
this._leafletMap.invalidateSize();
this._fitMap();
}
private _replaceTileLayer() {
const map = this._leafletMap;
const config = this._config;
const Leaflet = this.Leaflet;
if (!map || !config || !Leaflet || !this._tileLayer) {
return;
}
this._tileLayer = replaceTileLayer(
Leaflet,
map,
this._tileLayer,
this._config!.dark_mode ?? this.hass.themes.darkMode
);
}
private updateMap(oldConfig: MapCardConfig): void {
const map = this._leafletMap;
const config = this._config;
const Leaflet = this.Leaflet;
if (!map || !config || !Leaflet || !this._tileLayer) {
return;
}
if (this._config!.dark_mode !== oldConfig.dark_mode) {
this._replaceTileLayer();
}
if (
config.entities !== oldConfig.entities ||
config.geo_location_sources !== oldConfig.geo_location_sources
) {
this._drawEntities();
}
map.invalidateSize();
this._fitMap();
}
private _fitMap(): void {
if (!this._leafletMap || !this.Leaflet || !this._config || !this.hass) {
return;
}
const zoom = this._config.default_zoom;
if (this._mapItems.length === 0) {
this._leafletMap.setView(
new this.Leaflet.LatLng(
this.hass.config.latitude,
this.hass.config.longitude
),
zoom || 14
);
return;
}
const bounds = this.Leaflet.featureGroup(this._mapItems).getBounds();
this._leafletMap.fitBounds(bounds.pad(0.5));
if (zoom && this._leafletMap.getZoom() > zoom) {
this._leafletMap.setZoom(zoom);
}
}
private _getColor(entityId: string) {
let color;
if (this._colorDict[entityId]) {
color = this._colorDict[entityId];
} else {
color = this._colors[this._colorIndex];
this._colorIndex = (this._colorIndex + 1) % this._colors.length;
this._colorDict[entityId] = color;
} }
color = COLORS[this._colorIndex % COLORS.length];
this._colorIndex++;
this._colorDict[entityId] = color;
return color; return color;
} }
private _getEntities = memoizeOne( private _drawEntities(): void {
( const hass = this.hass;
states: HassEntities, const map = this._leafletMap;
config: MapCardConfig, const config = this._config;
configEntities?: string[] const Leaflet = this.Leaflet;
) => { if (!hass || !map || !config || !Leaflet) {
if (!states || !config) { return;
return undefined;
}
let entities = configEntities || [];
if (config.geo_location_sources) {
const geoEntities: string[] = [];
// Calculate visible geo location sources
const includesAll = config.geo_location_sources.includes("all");
for (const stateObj of Object.values(states)) {
if (
computeDomain(stateObj.entity_id) === "geo_location" &&
(includesAll ||
config.geo_location_sources.includes(stateObj.attributes.source))
) {
geoEntities.push(stateObj.entity_id);
}
}
entities = [...entities, ...geoEntities];
}
return entities.map((entity) => ({
entity_id: entity,
color: this._getColor(entity),
}));
} }
);
private _getHistoryPaths = memoizeOne( if (this._mapItems) {
( this._mapItems.forEach((marker) => marker.remove());
config: MapCardConfig, }
history?: HassEntity[][] const mapItems: Layer[] = (this._mapItems = []);
): HaMapPaths[] | undefined => {
if (!config.hours_to_show || !history) { if (this._mapZones) {
return undefined; this._mapZones.forEach((marker) => marker.remove());
}
const mapZones: Layer[] = (this._mapZones = []);
if (this._mapPaths) {
this._mapPaths.forEach((marker) => marker.remove());
}
const mapPaths: Layer[] = (this._mapPaths = []);
const allEntities = this._configEntities!.concat();
// Calculate visible geo location sources
if (config.geo_location_sources) {
const includesAll = config.geo_location_sources.includes("all");
for (const entityId of Object.keys(hass.states)) {
const stateObj = hass.states[entityId];
if (
computeDomain(entityId) === "geo_location" &&
(includesAll ||
config.geo_location_sources.includes(stateObj.attributes.source))
) {
allEntities.push({ entity: entityId });
}
} }
}
const paths: HaMapPaths[] = []; // DRAW history
if (this._config!.hours_to_show && this._history) {
for (const entityStates of history) { for (const entityStates of this._history) {
if (entityStates?.length <= 1) { if (entityStates?.length <= 1) {
continue; continue;
} }
const entityId = entityStates[0].entity_id;
// filter location data from states and remove all invalid locations // filter location data from states and remove all invalid locations
const points = entityStates.reduce( const path = entityStates.reduce(
(accumulator: LatLngTuple[], entityState) => { (accumulator: LatLngTuple[], state) => {
const latitude = entityState.attributes.latitude; const latitude = state.attributes.latitude;
const longitude = entityState.attributes.longitude; const longitude = state.attributes.longitude;
if (latitude && longitude) { if (latitude && longitude) {
accumulator.push([latitude, longitude] as LatLngTuple); accumulator.push([latitude, longitude] as LatLngTuple);
} }
@@ -295,15 +437,162 @@ class HuiMapCard extends LitElement implements LovelaceCard {
[] []
) as LatLngTuple[]; ) as LatLngTuple[];
paths.push({ // DRAW HISTORY
points, for (
color: this._getColor(entityStates[0].entity_id), let markerIndex = 0;
gradualOpacity: 0.8, markerIndex < path.length - 1;
}); markerIndex++
) {
const opacityStep = 0.8 / (path.length - 2);
const opacity = 0.2 + markerIndex * opacityStep;
// DRAW history path dots
mapPaths.push(
Leaflet.circleMarker(path[markerIndex], {
radius: 3,
color: this._getColor(entityId),
opacity,
interactive: false,
})
);
// DRAW history path lines
const line = [path[markerIndex], path[markerIndex + 1]];
mapPaths.push(
Leaflet.polyline(line, {
color: this._getColor(entityId),
opacity,
interactive: false,
})
);
}
} }
return paths;
} }
);
// DRAW entities
for (const entity of allEntities) {
const entityId = entity.entity;
const stateObj = hass.states[entityId];
if (!stateObj) {
continue;
}
const title = computeStateName(stateObj);
const {
latitude,
longitude,
passive,
icon,
radius,
entity_picture: entityPicture,
gps_accuracy: gpsAccuracy,
} = stateObj.attributes;
if (!(latitude && longitude)) {
continue;
}
if (computeStateDomain(stateObj) === "zone") {
// DRAW ZONE
if (passive) {
continue;
}
// create icon
let iconHTML = "";
if (icon) {
const el = document.createElement("ha-icon");
el.setAttribute("icon", icon);
iconHTML = el.outerHTML;
} else {
const el = document.createElement("span");
el.innerHTML = title;
iconHTML = el.outerHTML;
}
// create marker with the icon
mapZones.push(
Leaflet.marker([latitude, longitude], {
icon: Leaflet.divIcon({
html: iconHTML,
iconSize: [24, 24],
className: this._config!.dark_mode
? "dark"
: this._config!.dark_mode === false
? "light"
: "",
}),
interactive: false,
title,
})
);
// create circle around it
mapZones.push(
Leaflet.circle([latitude, longitude], {
interactive: false,
color: "#FF9800",
radius,
})
);
continue;
}
// DRAW ENTITY
// create icon
const entityName = title
.split(" ")
.map((part) => part[0])
.join("")
.substr(0, 3);
// create market with the icon
mapItems.push(
Leaflet.marker([latitude, longitude], {
icon: Leaflet.divIcon({
// Leaflet clones this element before adding it to the map. This messes up
// our Polymer object and we can't pass data through. Thus we hack like this.
html: `
<ha-entity-marker
entity-id="${entityId}"
entity-name="${entityName}"
entity-picture="${entityPicture || ""}"
entity-color="${this._getColor(entityId)}"
></ha-entity-marker>
`,
iconSize: [48, 48],
className: "",
}),
title: computeStateName(stateObj),
})
);
// create circle around if entity has accuracy
if (gpsAccuracy) {
mapItems.push(
Leaflet.circle([latitude, longitude], {
interactive: false,
color: this._getColor(entityId),
radius: gpsAccuracy,
})
);
}
}
this._mapItems.forEach((marker) => map.addLayer(marker));
this._mapZones.forEach((marker) => map.addLayer(marker));
this._mapPaths.forEach((marker) => map.addLayer(marker));
}
private async _attachObserver(): Promise<void> {
// Observe changes to map size and invalidate to prevent broken rendering
if (!this._resizeObserver) {
await installResizeObserver();
this._resizeObserver = new ResizeObserver(this._debouncedResizeListener);
}
this._resizeObserver.observe(this);
}
private async _getHistory(): Promise<void> { private async _getHistory(): Promise<void> {
this._date = new Date(); this._date = new Date();
@@ -312,7 +601,9 @@ class HuiMapCard extends LitElement implements LovelaceCard {
return; return;
} }
const entityIds = this._configEntities!.join(","); const entityIds = this._configEntities!.map((entity) => entity.entity).join(
","
);
const endTime = new Date(); const endTime = new Date();
const startTime = new Date(); const startTime = new Date();
startTime.setHours(endTime.getHours() - this._config!.hours_to_show!); startTime.setHours(endTime.getHours() - this._config!.hours_to_show!);
@@ -333,6 +624,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
if (stateHistory.length < 1) { if (stateHistory.length < 1) {
return; return;
} }
this._history = stateHistory; this._history = stateHistory;
} }
@@ -344,10 +636,13 @@ class HuiMapCard extends LitElement implements LovelaceCard {
this._history = undefined; this._history = undefined;
} else { } else {
// remove unused entities // remove unused entities
const configEntityIds = this._configEntities?.map(
(configEntity) => configEntity.entity
);
this._history = this._history!.reduce( this._history = this._history!.reduce(
(accumulator: HassEntity[][], entityStates) => { (accumulator: HassEntity[][], entityStates) => {
const entityId = entityStates[0].entity_id; const entityId = entityStates[0].entity_id;
if (this._configEntities?.includes(entityId)) { if (configEntityIds?.includes(entityId)) {
accumulator.push(entityStates); accumulator.push(entityStates);
} }
return accumulator; return accumulator;
@@ -365,7 +660,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
height: 100%; height: 100%;
} }
ha-map { #map {
z-index: 0; z-index: 0;
border: none; border: none;
position: absolute; position: absolute;
@@ -376,7 +671,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
background: inherit; background: inherit;
} }
mwc-icon-button { ha-icon-button {
position: absolute; position: absolute;
top: 75px; top: 75px;
left: 3px; left: 3px;
@@ -390,6 +685,14 @@ class HuiMapCard extends LitElement implements LovelaceCard {
:host([ispanel]) #root { :host([ispanel]) #root {
height: 100%; height: 100%;
} }
.dark {
color: #ffffff;
}
.light {
color: #000000;
}
`; `;
} }
} }

View File

@@ -33,18 +33,18 @@ export class HuiActionEditor extends LitElement {
@property() protected hass?: HomeAssistant; @property() protected hass?: HomeAssistant;
get _navigation_path(): string { get _navigation_path(): string {
const config = this.config as NavigateActionConfig | undefined; const config = this.config as NavigateActionConfig;
return config?.navigation_path || ""; return config.navigation_path || "";
} }
get _url_path(): string { get _url_path(): string {
const config = this.config as UrlActionConfig | undefined; const config = this.config as UrlActionConfig;
return config?.url_path || ""; return config.url_path || "";
} }
get _service(): string { get _service(): string {
const config = this.config as CallServiceActionConfig; const config = this.config as CallServiceActionConfig;
return config?.service || ""; return config.service || "";
} }
private _serviceAction = memoizeOne( private _serviceAction = memoizeOne(

View File

@@ -29,7 +29,6 @@ export class HuiInputListEditor extends LitElement {
.index=${index} .index=${index}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@blur=${this._consolidateEntries} @blur=${this._consolidateEntries}
@keydown=${this._handleKeyDown}
><ha-icon-button ><ha-icon-button
slot="suffix" slot="suffix"
class="clear-button" class="clear-button"
@@ -71,13 +70,6 @@ export class HuiInputListEditor extends LitElement {
}); });
} }
private _handleKeyDown(ev: KeyboardEvent) {
if (ev.key === "Enter") {
ev.stopPropagation();
this._consolidateEntries(ev);
}
}
private _consolidateEntries(ev: Event): void { private _consolidateEntries(ev: Event): void {
const target = ev.target! as EditorTarget; const target = ev.target! as EditorTarget;
if (target.value === "") { if (target.value === "") {

View File

@@ -15,8 +15,7 @@ import "../../components/hui-action-editor";
import "../../components/hui-entity-editor"; import "../../components/hui-entity-editor";
import "../../components/hui-theme-select-editor"; import "../../components/hui-theme-select-editor";
import { LovelaceCardEditor } from "../../types"; import { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct"; import { actionConfigStruct, EditorTarget } from "../types";
import { EditorTarget } from "../types";
import { configElementStyle } from "./config-elements-style"; import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = object({ const cardConfigStruct = object({

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