mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-13 11:19:25 +00:00
Compare commits
7 Commits
checkbox-s
...
20210528.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
cf03f103ab | ||
![]() |
4a8a7c997e | ||
![]() |
9612bc78fe | ||
![]() |
2b86137388 | ||
![]() |
8fdbe447c1 | ||
![]() |
764ae7e0b6 | ||
![]() |
6b7e78320d |
@@ -104,6 +104,5 @@
|
||||
"lit/attribute-value-entities": 0
|
||||
},
|
||||
"plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"],
|
||||
"processor": "disable/disable",
|
||||
"ignorePatterns": ["src/resources/lit-virtualizer/*"]
|
||||
"processor": "disable/disable"
|
||||
}
|
||||
|
6
.github/workflows/release.yaml
vendored
6
.github/workflows/release.yaml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
- published
|
||||
|
||||
env:
|
||||
WHEELS_TAG: 3.8-alpine3.12
|
||||
PYTHON_VERSION: 3.8
|
||||
NODE_VERSION: 12.1
|
||||
|
||||
@@ -63,9 +64,6 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
arch: ["aarch64", "armhf", "armv7", "amd64", "i386"]
|
||||
tag:
|
||||
- "3.8-alpine3.12"
|
||||
- "3.9-alpine3.13"
|
||||
steps:
|
||||
- name: Download requirements.txt
|
||||
uses: actions/download-artifact@v2
|
||||
@@ -75,7 +73,7 @@ jobs:
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@master
|
||||
with:
|
||||
tag: ${{ matrix.tag }}
|
||||
tag: ${{ env.WHEELS_TAG }}
|
||||
arch: ${{ matrix.arch }}
|
||||
wheels-host: ${{ secrets.WHEELS_HOST }}
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
|
21
.gitignore
vendored
21
.gitignore
vendored
@@ -1,17 +1,10 @@
|
||||
.DS_Store
|
||||
.reify-cache
|
||||
|
||||
# build
|
||||
build
|
||||
build-translations/*
|
||||
hass_frontend/*
|
||||
dist
|
||||
|
||||
# yarn
|
||||
.yarn
|
||||
yarn-error.log
|
||||
node_modules/*
|
||||
npm-debug.log
|
||||
.DS_Store
|
||||
hass_frontend/*
|
||||
.reify-cache
|
||||
|
||||
# Python stuff
|
||||
*.py[cod]
|
||||
@@ -21,8 +14,11 @@ npm-debug.log
|
||||
# venv stuff
|
||||
pyvenv.cfg
|
||||
pip-selfcheck.json
|
||||
venv/*
|
||||
venv
|
||||
.venv
|
||||
lib
|
||||
bin
|
||||
dist
|
||||
|
||||
# vscode
|
||||
.vscode/*
|
||||
@@ -35,8 +31,9 @@ src/cast/dev_const.ts
|
||||
|
||||
# Secrets
|
||||
.lokalise_token
|
||||
yarn-error.log
|
||||
|
||||
# asdf
|
||||
#asdf
|
||||
.tool-versions
|
||||
|
||||
# Home Assistant config
|
||||
|
@@ -52,7 +52,6 @@ module.exports.terserOptions = (latestBuild) => ({
|
||||
|
||||
module.exports.babelOptions = ({ latestBuild }) => ({
|
||||
babelrc: false,
|
||||
compact: false,
|
||||
presets: [
|
||||
!latestBuild && [
|
||||
"@babel/preset-env",
|
||||
@@ -80,6 +79,12 @@ module.exports.babelOptions = ({ latestBuild }) => ({
|
||||
].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) =>
|
||||
path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5");
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
// Tasks to run webpack.
|
||||
const fs = require("fs");
|
||||
const gulp = require("gulp");
|
||||
const webpack = require("webpack");
|
||||
const WebpackDevServer = require("webpack-dev-server");
|
||||
@@ -19,11 +18,6 @@ const bothBuilds = (createConfigFunc, params) => [
|
||||
createConfigFunc({ ...params, latestBuild: false }),
|
||||
];
|
||||
|
||||
const isWsl = fs
|
||||
.readFileSync("/proc/version", "utf-8")
|
||||
.toLocaleLowerCase()
|
||||
.includes("microsoft");
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* compiler: import("webpack").Compiler,
|
||||
@@ -85,7 +79,7 @@ const prodBuild = (conf) =>
|
||||
gulp.task("webpack-watch-app", () => {
|
||||
// This command will run forever because we don't close compiler
|
||||
webpack(createAppConfig({ isProdBuild: false, latestBuild: true })).watch(
|
||||
{ ignored: /build-translations/, poll: isWsl },
|
||||
{ ignored: /build-translations/ },
|
||||
doneHandler()
|
||||
);
|
||||
gulp.watch(
|
||||
@@ -143,7 +137,7 @@ gulp.task("webpack-watch-hassio", () => {
|
||||
isProdBuild: false,
|
||||
latestBuild: true,
|
||||
})
|
||||
).watch({ ignored: /build-translations/, poll: isWsl }, doneHandler());
|
||||
).watch({ ignored: /build-translations/ }, doneHandler());
|
||||
|
||||
gulp.watch(
|
||||
path.join(paths.translations_src, "en.json"),
|
||||
|
@@ -57,6 +57,7 @@ const createRollupConfig = ({
|
||||
babel({
|
||||
...bundle.babelOptions({ latestBuild }),
|
||||
extensions,
|
||||
exclude: bundle.babelExclude(),
|
||||
babelHelpers: isWDS ? "inline" : "bundled",
|
||||
}),
|
||||
string({
|
||||
|
@@ -47,6 +47,7 @@ const createWebpackConfig = ({
|
||||
rules: [
|
||||
{
|
||||
test: /\.m?js$|\.ts$/,
|
||||
exclude: bundle.babelExclude(),
|
||||
use: {
|
||||
loader: "babel-loader",
|
||||
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
|
||||
new webpack.NormalModuleReplacementPlugin(
|
||||
new RegExp(
|
||||
path.resolve(
|
||||
paths.polymer_dir,
|
||||
"src/resources/lit-virtualizer/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js"
|
||||
require.resolve(
|
||||
"@lit-labs/virtualizer/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js"
|
||||
)
|
||||
),
|
||||
path.resolve(paths.polymer_dir, "src/resources/EventTarget-ponyfill.js")
|
||||
|
@@ -28,11 +28,10 @@ const createConfigEntry = (
|
||||
title,
|
||||
source: "zeroconf",
|
||||
state: "loaded",
|
||||
connection_class: "local_push",
|
||||
supports_options: false,
|
||||
supports_unload: true,
|
||||
disabled_by: null,
|
||||
pref_disable_new_entities: false,
|
||||
pref_disable_polling: false,
|
||||
reason: null,
|
||||
...override,
|
||||
});
|
||||
@@ -65,9 +64,6 @@ const configPanelEntry = createConfigEntry("Config Panel", {
|
||||
const optionsFlowEntry = createConfigEntry("Options Flow", {
|
||||
supports_options: true,
|
||||
});
|
||||
const disabledPollingEntry = createConfigEntry("Disabled Polling", {
|
||||
pref_disable_polling: true,
|
||||
});
|
||||
const setupErrorEntry = createConfigEntry("Setup Error", {
|
||||
state: "setup_error",
|
||||
});
|
||||
@@ -140,7 +136,6 @@ const configEntries: Array<{
|
||||
{ items: [loadedEntry] },
|
||||
{ items: [configPanelEntry] },
|
||||
{ items: [optionsFlowEntry] },
|
||||
{ items: [disabledPollingEntry] },
|
||||
{ items: [nameAsDomainEntry] },
|
||||
{ items: [longNameEntry] },
|
||||
{ items: [longNonBreakingNameEntry] },
|
||||
|
@@ -3,7 +3,6 @@ import { ActionDetail } from "@material/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea";
|
||||
import { DEFAULT_SCHEMA, Type } from "js-yaml";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -12,7 +11,7 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/buttons/ha-progress-button";
|
||||
@@ -28,7 +27,6 @@ import {
|
||||
HassioAddonDetails,
|
||||
HassioAddonSetOptionParams,
|
||||
setHassioAddonOption,
|
||||
validateHassioAddonOption,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
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 ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
|
||||
new Type("!secret", {
|
||||
kind: "scalar",
|
||||
construct: (data) => `!secret ${data}`,
|
||||
}),
|
||||
]);
|
||||
|
||||
@customElement("hassio-addon-config")
|
||||
class HassioAddonConfig extends LitElement {
|
||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||
@@ -134,7 +125,6 @@ class HassioAddonConfig extends LitElement {
|
||||
></ha-form>`
|
||||
: html` <ha-yaml-editor
|
||||
@value-changed=${this._configChanged}
|
||||
.schema=${ADDON_YAML_SCHEMA}
|
||||
></ha-yaml-editor>`}
|
||||
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
|
||||
${!this._yamlMode ||
|
||||
@@ -279,14 +269,6 @@ class HassioAddonConfig extends LitElement {
|
||||
this._error = undefined;
|
||||
|
||||
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, {
|
||||
options: this._yamlMode ? this._editor?.value : this._options,
|
||||
});
|
||||
|
@@ -977,7 +977,6 @@ class HassioAddonInfo extends LitElement {
|
||||
showDialogSupervisorUpdate(this, {
|
||||
supervisor: this.supervisor,
|
||||
name: this.addon.name,
|
||||
slug: this.addon.slug,
|
||||
version: this.addon.version_latest,
|
||||
snapshotParams: {
|
||||
name: `addon_${this.addon.slug}_${this.addon.version}`,
|
||||
|
@@ -29,6 +29,7 @@ class SupervisorFormfieldLabel extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
@@ -5,7 +5,6 @@ import { customElement, property } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { formatDate } from "../../../src/common/datetime/format_date";
|
||||
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-formfield";
|
||||
import "../../../src/components/ha-radio";
|
||||
@@ -45,9 +44,6 @@ const _computeFolders = (folders): CheckboxItem[] => {
|
||||
if (folders.includes("share")) {
|
||||
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")) {
|
||||
list.push({ slug: "addons/local", name: "Local add-ons", checked: false });
|
||||
}
|
||||
@@ -68,8 +64,6 @@ const _computeAddons = (addons): AddonCheckboxItem[] =>
|
||||
export class SupervisorSnapshotContent extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public localize?: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) public supervisor?: Supervisor;
|
||||
|
||||
@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 onboarding = false;
|
||||
|
||||
@property() public snapshotName = "";
|
||||
|
||||
@property() public snapshotPassword = "";
|
||||
|
||||
@property() public confirmSnapshotPassword = "";
|
||||
|
||||
public willUpdate(changedProps) {
|
||||
super.willUpdate(changedProps);
|
||||
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 {
|
||||
if (!this.onboarding && !this.supervisor) {
|
||||
if (!this.supervisor) {
|
||||
return html``;
|
||||
}
|
||||
const foldersSection =
|
||||
@@ -128,16 +114,14 @@ export class SupervisorSnapshotContent extends LitElement {
|
||||
${this.snapshot
|
||||
? html`<div class="details">
|
||||
${this.snapshot.type === "full"
|
||||
? this._localize("full_snapshot")
|
||||
: this._localize("partial_snapshot")}
|
||||
? this.supervisor.localize("snapshot.full_snapshot")
|
||||
: this.supervisor.localize("snapshot.partial_snapshot")}
|
||||
(${Math.ceil(this.snapshot.size * 10) / 10 + " MB"})<br />
|
||||
${this.hass
|
||||
? formatDateTime(new Date(this.snapshot.date), this.hass.locale)
|
||||
: this.snapshot.date}
|
||||
${formatDateTime(new Date(this.snapshot.date), this.hass.locale)}
|
||||
</div>`
|
||||
: html`<paper-input
|
||||
name="snapshotName"
|
||||
.label=${this.supervisor?.localize("snapshot.name") || "Name"}
|
||||
.label=${this.supervisor.localize("snapshot.name")}
|
||||
.value=${this.snapshotName}
|
||||
@value-changed=${this._handleTextValueChanged}
|
||||
>
|
||||
@@ -145,11 +129,13 @@ export class SupervisorSnapshotContent extends LitElement {
|
||||
${!this.snapshot || this.snapshot.type === "full"
|
||||
? html`<div class="sub-header">
|
||||
${!this.snapshot
|
||||
? this._localize("type")
|
||||
: this._localize("select_type")}
|
||||
? this.supervisor.localize("snapshot.type")
|
||||
: this.supervisor.localize("snapshot.select_type")}
|
||||
</div>
|
||||
<div class="snapshot-types">
|
||||
<ha-formfield .label=${this._localize("full_snapshot")}>
|
||||
<ha-formfield
|
||||
.label=${this.supervisor.localize("snapshot.full_snapshot")}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="full"
|
||||
@@ -158,7 +144,9 @@ export class SupervisorSnapshotContent extends LitElement {
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield .label=${this._localize("partial_snapshot")}>
|
||||
<ha-formfield
|
||||
.label=${this.supervisor!.localize("snapshot.partial_snapshot")}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="partial"
|
||||
@@ -169,9 +157,9 @@ export class SupervisorSnapshotContent extends LitElement {
|
||||
</ha-formfield>
|
||||
</div>`
|
||||
: ""}
|
||||
${this.snapshotType === "partial"
|
||||
? html`<div class="partial-picker">
|
||||
${this.snapshot && this.snapshot.homeassistant
|
||||
${this.snapshot && this.snapshotType === "partial"
|
||||
? html`
|
||||
${this.snapshot.homeassistant
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
@@ -191,11 +179,15 @@ export class SupervisorSnapshotContent extends LitElement {
|
||||
</ha-formfield>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
${this.snapshotType === "partial"
|
||||
? html`
|
||||
${foldersSection?.templates.length
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${this._localize("folders")}
|
||||
.label=${this.supervisor.localize("snapshot.folders")}
|
||||
.iconPath=${mdiFolder}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
@@ -215,7 +207,7 @@ export class SupervisorSnapshotContent extends LitElement {
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${this._localize("addons")}
|
||||
.label=${this.supervisor.localize("snapshot.addons")}
|
||||
.iconPath=${mdiPuzzle}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
@@ -231,44 +223,29 @@ export class SupervisorSnapshotContent extends LitElement {
|
||||
<div class="section-content">${addonsSection.templates}</div>
|
||||
`
|
||||
: ""}
|
||||
</div> `
|
||||
: ""}
|
||||
${this.snapshotType === "partial" &&
|
||||
(!this.snapshot || this.snapshotHasPassword)
|
||||
? html`<hr />`
|
||||
`
|
||||
: ""}
|
||||
${!this.snapshot
|
||||
? html`<ha-formfield
|
||||
class="password"
|
||||
.label=${this._localize("password_protection")}
|
||||
.label=${this.supervisor.localize("snapshot.password_protection")}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.snapshotHasPassword}
|
||||
@change=${this._toggleHasPassword}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>`
|
||||
</ha-checkbox
|
||||
></ha-formfield>`
|
||||
: ""}
|
||||
${this.snapshotHasPassword
|
||||
? html`
|
||||
<paper-input
|
||||
.label=${this._localize("password")}
|
||||
.label=${this.supervisor.localize("snapshot.password")}
|
||||
type="password"
|
||||
name="snapshotPassword"
|
||||
.value=${this.snapshotPassword}
|
||||
@value-changed=${this._handleTextValueChanged}
|
||||
>
|
||||
</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 {
|
||||
return css`
|
||||
.partial-picker ha-formfield {
|
||||
ha-checkbox {
|
||||
--mdc-checkbox-touch-target-size: 16px;
|
||||
display: block;
|
||||
margin: 4px 12px 8px 0;
|
||||
}
|
||||
.partial-picker ha-checkbox {
|
||||
--mdc-checkbox-touch-target-size: 32px;
|
||||
}
|
||||
.partial-picker {
|
||||
display: block;
|
||||
margin: 0px -6px;
|
||||
ha-formfield {
|
||||
display: contents;
|
||||
}
|
||||
supervisor-formfield-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
hr {
|
||||
border-color: var(--divider-color);
|
||||
border-bottom: none;
|
||||
margin: 16px 0;
|
||||
paper-input[type="password"] {
|
||||
display: block;
|
||||
margin: 4px 0 4px 16px;
|
||||
}
|
||||
.details {
|
||||
color: var(--secondary-text-color);
|
||||
@@ -301,15 +275,13 @@ export class SupervisorSnapshotContent extends LitElement {
|
||||
.section-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 30px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
ha-formfield.password {
|
||||
display: block;
|
||||
margin: 0 -14px -16px;
|
||||
.security {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.snapshot-types {
|
||||
display: flex;
|
||||
margin-left: -13px;
|
||||
}
|
||||
.sub-header {
|
||||
margin-top: 8px;
|
||||
@@ -328,9 +300,6 @@ export class SupervisorSnapshotContent extends LitElement {
|
||||
|
||||
if (this.snapshotHasPassword) {
|
||||
data.password = this.snapshotPassword;
|
||||
if (!this.snapshot) {
|
||||
data.confirm_password = this.confirmSnapshotPassword;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.snapshotType === "full") {
|
||||
@@ -362,7 +331,7 @@ export class SupervisorSnapshotContent extends LitElement {
|
||||
const addons =
|
||||
section === "addons"
|
||||
? new Map(
|
||||
this.supervisor?.addon.addons.map((item) => [item.slug, item])
|
||||
this.supervisor!.addon.addons.map((item) => [item.slug, item])
|
||||
)
|
||||
: undefined;
|
||||
let checkedItems = 0;
|
||||
@@ -372,7 +341,6 @@ export class SupervisorSnapshotContent extends LitElement {
|
||||
.label=${item.name}
|
||||
.iconPath=${section === "addons" ? mdiPuzzle : mdiFolder}
|
||||
.imageUrl=${section === "addons" &&
|
||||
!this.onboarding &&
|
||||
atLeastVersion(this.hass.config.version, 0, 105) &&
|
||||
addons?.get(item.slug)?.icon
|
||||
? `/api/hassio/addons/${item.slug}/icon`
|
||||
|
@@ -161,7 +161,6 @@ export class HassioUpdate extends LitElement {
|
||||
showDialogSupervisorUpdate(this, {
|
||||
supervisor: this.supervisor,
|
||||
name: "Home Assistant Core",
|
||||
slug: "core",
|
||||
version: this.supervisor.core.version_latest,
|
||||
snapshotParams: {
|
||||
name: `core_${this.supervisor.core.version}`,
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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,
|
||||
});
|
||||
};
|
@@ -244,6 +244,9 @@ class HassioRegistriesDialog extends LitElement {
|
||||
mwc-list-item span[slot="secondary"] {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-paper-dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -150,6 +150,9 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
mwc-button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
ha-paper-dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
ha-circular-progress {
|
||||
display: block;
|
||||
margin: 32px;
|
||||
|
@@ -95,25 +95,16 @@ class HassioCreateSnapshotDialog extends LitElement {
|
||||
this._creatingSnapshot = true;
|
||||
|
||||
this._error = "";
|
||||
if (snapshotDetails.password && !snapshotDetails.password.length) {
|
||||
if (
|
||||
this._snapshotContent.snapshotHasPassword &&
|
||||
!this._snapshotContent.snapshotPassword.length
|
||||
) {
|
||||
this._error = this._dialogParams!.supervisor.localize(
|
||||
"snapshot.enter_password"
|
||||
);
|
||||
this._creatingSnapshot = false;
|
||||
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 {
|
||||
if (this._snapshotContent.snapshotType === "full") {
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import { ActionDetail } from "@material/mwc-list";
|
||||
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 { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { slugify } from "../../../../src/common/string/slugify";
|
||||
import "../../../../src/components/buttons/ha-progress-button";
|
||||
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 { getSignedPath } from "../../../../src/data/auth";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { fileDownload } from "../../../../src/util/file_download";
|
||||
import "../../components/supervisor-snapshot-content";
|
||||
import type { SupervisorSnapshotContent } from "../../components/supervisor-snapshot-content";
|
||||
import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot";
|
||||
@@ -67,24 +66,14 @@ class HassioSnapshotDialog
|
||||
open
|
||||
scrimClickAction
|
||||
@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
|
||||
? html` <ha-circular-progress active></ha-circular-progress>`
|
||||
: html`<supervisor-snapshot-content
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this._dialogParams.supervisor}
|
||||
.snapshot=${this._snapshot}
|
||||
.onboarding=${this._dialogParams.onboarding || false}
|
||||
.localize=${this._dialogParams.localize}
|
||||
>
|
||||
</supervisor-snapshot-content>`}
|
||||
${this._error ? html`<p class="error">Error: ${this._error}</p>` : ""}
|
||||
@@ -97,20 +86,18 @@ class HassioSnapshotDialog
|
||||
Restore
|
||||
</mwc-button>
|
||||
|
||||
${!this._dialogParams.onboarding
|
||||
? html`<ha-button-menu
|
||||
fixed
|
||||
slot="primaryAction"
|
||||
@action=${this._handleMenuAction}
|
||||
@closed=${(ev: Event) => ev.stopPropagation()}
|
||||
>
|
||||
<mwc-icon-button slot="trigger" alt="menu">
|
||||
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<mwc-list-item>Download Snapshot</mwc-list-item>
|
||||
<mwc-list-item class="error">Delete Snapshot</mwc-list-item>
|
||||
</ha-button-menu>`
|
||||
: ""}
|
||||
<ha-button-menu
|
||||
fixed
|
||||
slot="primaryAction"
|
||||
@action=${this._handleMenuAction}
|
||||
@closed=${(ev: Event) => ev.stopPropagation()}
|
||||
>
|
||||
<mwc-icon-button slot="trigger" alt="menu">
|
||||
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<mwc-list-item>Download Snapshot</mwc-list-item>
|
||||
<mwc-list-item class="error">Delete Snapshot</mwc-list-item>
|
||||
</ha-button-menu>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
@@ -127,12 +114,6 @@ class HassioSnapshotDialog
|
||||
display: block;
|
||||
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(
|
||||
this,
|
||||
signedPath.path,
|
||||
`home_assistant_snapshot_${slugify(this._computeName)}.tar`
|
||||
);
|
||||
const a = document.createElement("a");
|
||||
a.href = signedPath.path;
|
||||
a.download = `home_assistant_snapshot_${slugify(this._computeName)}.tar`;
|
||||
this.shadowRoot!.appendChild(a);
|
||||
a.click();
|
||||
this.shadowRoot!.removeChild(a);
|
||||
}
|
||||
|
||||
private get _computeName() {
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { LocalizeFunc } from "../../../../src/common/translations/localize";
|
||||
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
|
||||
export interface HassioSnapshotDialogParams {
|
||||
@@ -7,7 +6,6 @@ export interface HassioSnapshotDialogParams {
|
||||
onDelete?: () => void;
|
||||
onboarding?: boolean;
|
||||
supervisor?: Supervisor;
|
||||
localize?: LocalizeFunc;
|
||||
}
|
||||
|
||||
export const showHassioSnapshotDialog = (
|
||||
|
@@ -2,32 +2,19 @@ import "@material/mwc-button/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-checkbox";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import "../../../../src/components/ha-switch";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
ignoreSupervisorError,
|
||||
} from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
SupervisorFrontendPrefrences,
|
||||
fetchSupervisorFrontendPreferences,
|
||||
saveSupervisorFrontendPreferences,
|
||||
} from "../../../../src/data/supervisor/supervisor";
|
||||
import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
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")
|
||||
class DialogSupervisorUpdate extends LitElement {
|
||||
@@ -35,12 +22,12 @@ class DialogSupervisorUpdate extends LitElement {
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _createSnapshot = true;
|
||||
|
||||
@state() private _action: "snapshot" | "update" | null = null;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _frontendPrefrences?: SupervisorFrontendPrefrences;
|
||||
|
||||
@state()
|
||||
private _dialogParams?: SupervisorDialogSupervisorUpdateParams;
|
||||
|
||||
@@ -49,17 +36,14 @@ class DialogSupervisorUpdate extends LitElement {
|
||||
): Promise<void> {
|
||||
this._opened = true;
|
||||
this._dialogParams = params;
|
||||
this._frontendPrefrences = await fetchSupervisorFrontendPreferences(
|
||||
this.hass
|
||||
);
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._action = null;
|
||||
this._createSnapshot = true;
|
||||
this._error = undefined;
|
||||
this._dialogParams = undefined;
|
||||
this._frontendPrefrences = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -72,7 +56,7 @@ class DialogSupervisorUpdate extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._dialogParams || !this._frontendPrefrences) {
|
||||
if (!this._dialogParams) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
@@ -98,16 +82,6 @@ class DialogSupervisorUpdate extends LitElement {
|
||||
</div>
|
||||
|
||||
<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">
|
||||
${this._dialogParams.supervisor.localize(
|
||||
"dialog.update.snapshot"
|
||||
@@ -120,6 +94,12 @@ class DialogSupervisorUpdate extends LitElement {
|
||||
this._dialogParams.name
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
.checked=${this._createSnapshot}
|
||||
haptic
|
||||
@click=${this._toggleSnapshot}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
|
||||
${this._dialogParams.supervisor.localize("common.cancel")}
|
||||
@@ -153,27 +133,12 @@ class DialogSupervisorUpdate extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _toggleSnapshot(): Promise<void> {
|
||||
this._frontendPrefrences!.snapshot_before_update[
|
||||
this._dialogParams!.slug
|
||||
] = !snapshot_before_update(
|
||||
this._dialogParams!.slug,
|
||||
this._frontendPrefrences!
|
||||
);
|
||||
|
||||
await saveSupervisorFrontendPreferences(
|
||||
this.hass,
|
||||
this._frontendPrefrences!
|
||||
);
|
||||
private _toggleSnapshot() {
|
||||
this._createSnapshot = !this._createSnapshot;
|
||||
}
|
||||
|
||||
private async _update() {
|
||||
if (
|
||||
snapshot_before_update(
|
||||
this._dialogParams!.slug,
|
||||
this._frontendPrefrences!
|
||||
)
|
||||
) {
|
||||
if (this._createSnapshot) {
|
||||
this._action = "snapshot";
|
||||
try {
|
||||
await createHassioPartialSnapshot(
|
||||
|
@@ -5,7 +5,6 @@ export interface SupervisorDialogSupervisorUpdateParams {
|
||||
supervisor: Supervisor;
|
||||
name: string;
|
||||
version: string;
|
||||
slug: string;
|
||||
snapshotParams: any;
|
||||
updateHandler: () => Promise<void>;
|
||||
}
|
||||
|
@@ -97,23 +97,16 @@ class HassioIngressView extends LitElement {
|
||||
title: requestedAddon,
|
||||
});
|
||||
await nextRender();
|
||||
navigate("/hassio/store", { replace: true });
|
||||
history.back();
|
||||
return;
|
||||
}
|
||||
if (!addonInfo.version) {
|
||||
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) {
|
||||
if (!addonInfo.ingress) {
|
||||
await showAlertDialog(this, {
|
||||
text: this.supervisor.localize("my.error_addon_no_ingress"),
|
||||
title: addonInfo.name,
|
||||
});
|
||||
await nextRender();
|
||||
navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true });
|
||||
history.back();
|
||||
} else {
|
||||
navigate(`/hassio/ingress/${addonInfo.slug}`, { replace: true });
|
||||
}
|
||||
|
@@ -1,17 +1,15 @@
|
||||
import "@material/mwc-button";
|
||||
import { ActionDetail } from "@material/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
|
||||
import { mdiDotsVertical, mdiPlus } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import relativeTime from "../../../src/common/datetime/relative_time";
|
||||
@@ -19,25 +17,18 @@ import { HASSDomEvent } from "../../../src/common/dom/fire_event";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
} from "../../../src/components/data-table/ha-data-table";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-fab";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import {
|
||||
fetchHassioSnapshots,
|
||||
friendlyFolderName,
|
||||
HassioSnapshot,
|
||||
reloadHassioSnapshots,
|
||||
removeSnapshot,
|
||||
} from "../../../src/data/hassio/snapshot";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
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 { HomeAssistant, Route } from "../../../src/types";
|
||||
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;
|
||||
|
||||
@state() private _selectedSnapshots: string[] = [];
|
||||
private _firstUpdatedCalled = false;
|
||||
|
||||
@state() private _snapshots?: HassioSnapshot[] = [];
|
||||
|
||||
@query("hass-tabs-subpage-data-table", true)
|
||||
private _dataTable!: HaTabsSubpageDataTable;
|
||||
|
||||
private _firstUpdatedCalled = false;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.hass && this._firstUpdatedCalled) {
|
||||
@@ -167,9 +153,7 @@ export class HassioSnapshots extends LitElement {
|
||||
.data=${this._snapshotData(this._snapshots || [])}
|
||||
id="slug"
|
||||
@row-click=${this._handleRowClicked}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
clickable
|
||||
selectable
|
||||
hasFab
|
||||
main-page
|
||||
supervisor
|
||||
@@ -192,45 +176,6 @@ export class HassioSnapshots extends LitElement {
|
||||
: ""}
|
||||
</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
|
||||
slot="fab"
|
||||
@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() {
|
||||
showSnapshotUploadDialog(this, {
|
||||
showSnapshot: (slug: string) =>
|
||||
@@ -277,35 +216,6 @@ export class HassioSnapshots extends LitElement {
|
||||
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>) {
|
||||
const slug = ev.detail.id;
|
||||
showHassioSnapshotDialog(this, {
|
||||
@@ -334,45 +244,7 @@ export class HassioSnapshots extends LitElement {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
return [haStyle, hassioStyle];
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -164,7 +164,6 @@ class HassioCoreInfo extends LitElement {
|
||||
showDialogSupervisorUpdate(this, {
|
||||
supervisor: this.supervisor,
|
||||
name: "Home Assistant Core",
|
||||
slug: "core",
|
||||
version: this.supervisor.core.version_latest,
|
||||
snapshotParams: {
|
||||
name: `core_${this.supervisor.core.version}`,
|
||||
|
@@ -2,6 +2,7 @@ import "@material/mwc-button";
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import { dump } from "js-yaml";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -40,8 +41,8 @@ import {
|
||||
roundWithOneDecimal,
|
||||
} from "../../../src/util/calculate";
|
||||
import "../components/supervisor-metric";
|
||||
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
|
||||
import { showNetworkDialog } from "../dialogs/network/show-dialog-network";
|
||||
import { showHassioHardwareDialog } from "../dialogs/hardware/show-dialog-hassio-hardware";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-host-info")
|
||||
@@ -228,19 +229,20 @@ class HassioHostInfo extends LitElement {
|
||||
}
|
||||
|
||||
private async _showHardware(): Promise<void> {
|
||||
let hardware;
|
||||
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) {
|
||||
await showAlertDialog(this, {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"system.host.failed_to_get_hardware_list"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
return;
|
||||
}
|
||||
showHassioHardwareDialog(this, { supervisor: this.supervisor, hardware });
|
||||
}
|
||||
|
||||
private async _hostReboot(ev: CustomEvent): Promise<void> {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
"*.ts": () => "tsc -p tsconfig.json",
|
||||
"*.{js,ts}": "eslint --fix",
|
||||
"!(/translations)*.{js,ts,json,css,md,html}": "prettier --write",
|
||||
};
|
||||
|
@@ -66,9 +66,12 @@
|
||||
"@polymer/iron-autogrow-textarea": "^3.0.1",
|
||||
"@polymer/iron-flex-layout": "^3.0.1",
|
||||
"@polymer/iron-icon": "^3.0.1",
|
||||
"@polymer/iron-image": "^3.0.1",
|
||||
"@polymer/iron-input": "^3.0.1",
|
||||
"@polymer/iron-label": "^3.0.1",
|
||||
"@polymer/iron-overlay-behavior": "^3.0.2",
|
||||
"@polymer/iron-resizable-behavior": "^3.0.1",
|
||||
"@polymer/paper-card": "^3.0.1",
|
||||
"@polymer/paper-checkbox": "^3.1.0",
|
||||
"@polymer/paper-dialog": "^3.0.1",
|
||||
"@polymer/paper-dialog-behavior": "^3.0.1",
|
||||
@@ -106,7 +109,7 @@
|
||||
"fecha": "^4.2.0",
|
||||
"fuse.js": "^6.0.0",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^1.0.5",
|
||||
"hls.js": "^1.0.4",
|
||||
"home-assistant-js-websocket": "^5.10.0",
|
||||
"idb-keyval": "^5.0.5",
|
||||
"intl-messageformat": "^9.6.16",
|
||||
|
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20210603.0",
|
||||
version="20210528.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
@@ -6,7 +6,8 @@ export type LeafletDrawModuleType = typeof import("leaflet-draw");
|
||||
|
||||
export const setupLeafletMap = async (
|
||||
mapElement: HTMLElement,
|
||||
darkMode?: boolean
|
||||
darkMode?: boolean,
|
||||
draw = false
|
||||
): Promise<[Map, LeafletModuleType, TileLayer]> => {
|
||||
if (!mapElement.parentNode) {
|
||||
throw new Error("Cannot setup Leaflet map on disconnected element");
|
||||
@@ -16,6 +17,10 @@ export const setupLeafletMap = async (
|
||||
.default as LeafletModuleType;
|
||||
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
|
||||
|
||||
if (draw) {
|
||||
await import("leaflet-draw");
|
||||
}
|
||||
|
||||
const map = Leaflet.map(mapElement);
|
||||
const style = document.createElement("link");
|
||||
style.setAttribute("href", "/static/images/leaflet/leaflet.css");
|
||||
|
@@ -89,6 +89,8 @@ export const domainIcon = (
|
||||
}
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
@@ -1,16 +1,9 @@
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import { mdiClose, mdiMagnify } from "@mdi/js";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import "../../components/ha-svg-icon";
|
||||
import { fireEvent } from "../dom/fire_event";
|
||||
|
||||
@@ -34,11 +27,18 @@ class SearchInput extends LitElement {
|
||||
this.shadowRoot!.querySelector("paper-input")!.focus();
|
||||
}
|
||||
|
||||
@query("paper-input", true) private _input!: PaperInputElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
.no-underline:not(.focused) {
|
||||
--paper-input-container-underline: {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<paper-input
|
||||
class=${classMap({ "no-underline": this.noUnderline })}
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label || "Search"}
|
||||
.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) {
|
||||
fireEvent(this, "value-changed", { value: String(value) });
|
||||
}
|
||||
|
@@ -1,2 +0,0 @@
|
||||
export const escapeRegExp = (text: string): string =>
|
||||
text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
@@ -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);
|
@@ -1,6 +1,11 @@
|
||||
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 = () =>
|
||||
refine(string(), "entity ID (domain.entity)", isEntityId);
|
||||
|
@@ -1,5 +1,10 @@
|
||||
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);
|
||||
|
@@ -86,15 +86,11 @@ export const computeLocalize = async (
|
||||
| undefined;
|
||||
|
||||
if (!translatedMessage) {
|
||||
try {
|
||||
translatedMessage = new IntlMessageFormat(
|
||||
translatedValue,
|
||||
language,
|
||||
formats
|
||||
);
|
||||
} catch (err) {
|
||||
return "Translation error: " + err.message;
|
||||
}
|
||||
translatedMessage = new IntlMessageFormat(
|
||||
translatedValue,
|
||||
language,
|
||||
formats
|
||||
);
|
||||
cache._localizationCache[messageKey] = translatedMessage;
|
||||
}
|
||||
|
||||
|
@@ -4,25 +4,29 @@
|
||||
// be triggered. The function will be called after it stops being called for
|
||||
// N milliseconds. If `immediate` is passed, trigger the function on the
|
||||
// leading edge, instead of the trailing.
|
||||
|
||||
export const debounce = <T extends any[]>(
|
||||
func: (...args: T) => void,
|
||||
wait: number,
|
||||
// eslint-disable-next-line: ban-types
|
||||
export const debounce = <T extends (...args) => unknown>(
|
||||
func: T,
|
||||
wait,
|
||||
immediate = false
|
||||
) => {
|
||||
let timeout: number | undefined;
|
||||
return (...args: T): void => {
|
||||
): T => {
|
||||
let timeout;
|
||||
// @ts-ignore
|
||||
return function (...args) {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const context = this;
|
||||
const later = () => {
|
||||
timeout = undefined;
|
||||
timeout = null;
|
||||
if (!immediate) {
|
||||
func(...args);
|
||||
func.apply(context, args);
|
||||
}
|
||||
};
|
||||
const callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = window.setTimeout(later, wait);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) {
|
||||
func(...args);
|
||||
func.apply(context, args);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@@ -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 {
|
||||
css,
|
||||
@@ -246,7 +246,7 @@ export class HaDataTable extends LitElement {
|
||||
aria-rowcount=${this._filteredData.length + 1}
|
||||
style=${styleMap({
|
||||
height: this.autoHeight
|
||||
? `${(this._filteredData.length || 1) * 53 + 53}px`
|
||||
? `${(this._filteredData.length || 1) * 53 + 57}px`
|
||||
: `calc(100% - ${this._headerHeight}px)`,
|
||||
})}
|
||||
>
|
||||
@@ -340,10 +340,11 @@ export class HaDataTable extends LitElement {
|
||||
${scroll({
|
||||
items: this._items,
|
||||
layout: Layout1d,
|
||||
// @ts-expect-error
|
||||
renderItem: (row: DataTableRowData, index) => {
|
||||
// not sure how this happens...
|
||||
if (!row) {
|
||||
return html``;
|
||||
return "";
|
||||
}
|
||||
if (row.append) {
|
||||
return html`
|
||||
@@ -919,11 +920,13 @@ export class HaDataTable extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.scroller {
|
||||
display: flex;
|
||||
position: relative;
|
||||
contain: strict;
|
||||
height: calc(100% - 57px);
|
||||
}
|
||||
|
||||
.mdc-data-table__table.auto-height .scroller {
|
||||
overflow-y: hidden !important;
|
||||
.mdc-data-table__table:not(.auto-height) .scroller {
|
||||
overflow: auto;
|
||||
}
|
||||
.grows {
|
||||
flex-grow: 1;
|
||||
|
@@ -4,6 +4,7 @@ import {
|
||||
fetchDeviceActions,
|
||||
localizeDeviceAutomationAction,
|
||||
} from "../../data/device_automation";
|
||||
import "../ha-paper-dropdown-menu";
|
||||
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
||||
|
||||
@customElement("ha-device-action-picker")
|
||||
|
@@ -4,6 +4,7 @@ import {
|
||||
fetchDeviceConditions,
|
||||
localizeDeviceAutomationCondition,
|
||||
} from "../../data/device_automation";
|
||||
import "../ha-paper-dropdown-menu";
|
||||
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
||||
|
||||
@customElement("ha-device-condition-picker")
|
||||
|
@@ -4,6 +4,7 @@ import {
|
||||
fetchDeviceTriggers,
|
||||
localizeDeviceAutomationTrigger,
|
||||
} from "../../data/device_automation";
|
||||
import "../ha-paper-dropdown-menu";
|
||||
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
||||
|
||||
@customElement("ha-device-trigger-picker")
|
||||
|
@@ -54,17 +54,17 @@ class HaAttributes extends LitElement {
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
${this.stateObj.attributes.attribution
|
||||
? html`
|
||||
<div class="attribution">
|
||||
${this.stateObj.attributes.attribution}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</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 {
|
||||
color: var(--secondary-text-color);
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
pre {
|
||||
font-family: inherit;
|
||||
|
@@ -14,17 +14,12 @@ class HaExpansionPanel extends LitElement {
|
||||
|
||||
@property() header?: string;
|
||||
|
||||
@property() secondary?: string;
|
||||
|
||||
@query(".container") private _container!: HTMLDivElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="summary" @click=${this._toggleContainer}>
|
||||
<slot class="header" name="header">
|
||||
${this.header}
|
||||
<slot class="secondary" name="secondary">${this.secondary}</slot>
|
||||
</slot>
|
||||
<slot name="header">${this.header}</slot>
|
||||
<ha-svg-icon
|
||||
.path=${mdiChevronDown}
|
||||
class="summary-icon ${classMap({ expanded: this.expanded })}"
|
||||
@@ -111,16 +106,6 @@ class HaExpansionPanel extends LitElement {
|
||||
.container.expanded {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
display: block;
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -7,14 +7,14 @@ import { afterNextRender } from "../common/util/render-status";
|
||||
import { FrontendLocaleData } from "../data/translation";
|
||||
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 percentage = getValueInPercentage(normalize(value, min, max), min, max);
|
||||
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")
|
||||
export class Gauge extends LitElement {
|
||||
@property({ type: Number }) public min = 0;
|
||||
|
@@ -13,11 +13,6 @@ import { nextRender } from "../common/util/render-status";
|
||||
import { getExternalConfig } from "../external_app/external_config";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
type HlsLite = Omit<
|
||||
HlsType,
|
||||
"subtitleTrackController" | "audioTrackController" | "emeController"
|
||||
>;
|
||||
|
||||
@customElement("ha-hls-player")
|
||||
class HaHLSPlayer extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -44,7 +39,7 @@ class HaHLSPlayer extends LitElement {
|
||||
|
||||
@state() private _attached = false;
|
||||
|
||||
private _hlsPolyfillInstance?: HlsLite;
|
||||
private _hlsPolyfillInstance?: HlsType;
|
||||
|
||||
private _useExoPlayer = false;
|
||||
|
||||
@@ -108,8 +103,7 @@ class HaHLSPlayer extends LitElement {
|
||||
const useExoPlayerPromise = this._getUseExoPlayer();
|
||||
const masterPlaylistPromise = fetch(this.url);
|
||||
|
||||
const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min.js"))
|
||||
.default;
|
||||
const Hls = (await import("hls.js")).default;
|
||||
let hlsSupported = Hls.isSupported();
|
||||
|
||||
if (!hlsSupported) {
|
||||
@@ -188,7 +182,7 @@ class HaHLSPlayer extends LitElement {
|
||||
url: string
|
||||
) {
|
||||
const hls = new Hls({
|
||||
backBufferLength: 60,
|
||||
liveBackBufferLength: 60,
|
||||
fragLoadingTimeOut: 30000,
|
||||
manifestLoadingTimeOut: 30000,
|
||||
levelLoadingTimeOut: 30000,
|
||||
|
@@ -1,12 +1,5 @@
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
nothing,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, state, property } from "lit/decorators";
|
||||
import {
|
||||
Adapter,
|
||||
@@ -24,19 +17,18 @@ import "./ha-icon";
|
||||
|
||||
const format_addresses = (
|
||||
addresses: IPv6ConfiguredAddress[] | IPv4ConfiguredAddress[]
|
||||
): TemplateResult =>
|
||||
html`${addresses.map((address, i) => [
|
||||
html`<span>${address.address}/${address.network_prefix}</span>`,
|
||||
i < addresses.length - 1 ? ", " : nothing,
|
||||
])}`;
|
||||
): TemplateResult[] =>
|
||||
addresses.map(
|
||||
(address) => html`<span>${address.address}/${address.network_prefix}</span>`
|
||||
);
|
||||
|
||||
const format_auto_detected_interfaces = (
|
||||
adapters: Adapter[]
|
||||
): Array<TemplateResult | string> =>
|
||||
adapters.map((adapter) =>
|
||||
adapter.auto
|
||||
? html`${adapter.name}
|
||||
(${format_addresses([...adapter.ipv4, ...adapter.ipv6])})`
|
||||
? html`${adapter.name} (${format_addresses(adapter.ipv4)}
|
||||
${format_addresses(adapter.ipv6)} )`
|
||||
: ""
|
||||
);
|
||||
|
||||
@@ -96,7 +88,8 @@ export class HaNetwork extends LitElement {
|
||||
: ""}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${format_addresses([...adapter.ipv4, ...adapter.ipv6])}
|
||||
${format_addresses(adapter.ipv4)}
|
||||
${format_addresses(adapter.ipv6)}
|
||||
</span>
|
||||
</ha-settings-row>`
|
||||
)
|
||||
|
@@ -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 { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@@ -20,8 +20,6 @@ const isEmpty = (obj: Record<string, unknown>): boolean => {
|
||||
export class HaYamlEditor extends LitElement {
|
||||
@property() public value?: any;
|
||||
|
||||
@property({ attribute: false }) public yamlSchema: Schema = DEFAULT_SCHEMA;
|
||||
|
||||
@property() public defaultValue?: any;
|
||||
|
||||
@property() public isValid = true;
|
||||
@@ -32,10 +30,7 @@ export class HaYamlEditor extends LitElement {
|
||||
|
||||
public setValue(value): void {
|
||||
try {
|
||||
this._yaml =
|
||||
value && !isEmpty(value)
|
||||
? dump(value, { schema: this.yamlSchema })
|
||||
: "";
|
||||
this._yaml = value && !isEmpty(value) ? dump(value) : "";
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err, value);
|
||||
@@ -72,7 +67,7 @@ export class HaYamlEditor extends LitElement {
|
||||
|
||||
if (this._yaml) {
|
||||
try {
|
||||
parsed = load(this._yaml, { schema: this.yamlSchema });
|
||||
parsed = load(this._yaml);
|
||||
} catch (err) {
|
||||
// Invalid YAML
|
||||
isValid = false;
|
||||
|
@@ -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);
|
299
src/components/map/ha-location-editor.ts
Normal file
299
src/components/map/ha-location-editor.ts
Normal 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;
|
||||
}
|
||||
}
|
@@ -3,8 +3,10 @@ import {
|
||||
DivIcon,
|
||||
DragEndEvent,
|
||||
LatLng,
|
||||
Map,
|
||||
Marker,
|
||||
MarkerOptions,
|
||||
TileLayer,
|
||||
} from "leaflet";
|
||||
import {
|
||||
css,
|
||||
@@ -14,13 +16,15 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-map";
|
||||
import type { HaMap } from "./ha-map";
|
||||
import {
|
||||
LeafletModuleType,
|
||||
replaceTileLayer,
|
||||
setupLeafletMap,
|
||||
} from "../../common/dom/setup-leaflet-map";
|
||||
import { defaultRadiusColor } from "../../data/zone";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@@ -47,40 +51,38 @@ export interface MarkerLocation {
|
||||
export class HaLocationsEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public locations?: MarkerLocation[];
|
||||
@property() public locations?: MarkerLocation[];
|
||||
|
||||
@property({ type: Boolean }) public autoFit = false;
|
||||
|
||||
@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;
|
||||
public fitZoom = 16;
|
||||
|
||||
// eslint-disable-next-line
|
||||
private Leaflet?: LeafletModuleType;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// eslint-disable-next-line
|
||||
private _leafletMap?: Map;
|
||||
|
||||
import("leaflet").then((module) => {
|
||||
import("leaflet-draw").then(() => {
|
||||
this.Leaflet = module.default as LeafletModuleType;
|
||||
this._updateMarkers();
|
||||
this.updateComplete.then(() => this.fitMap());
|
||||
});
|
||||
});
|
||||
}
|
||||
private _tileLayer?: TileLayer;
|
||||
|
||||
private _locationMarkers?: { [key: string]: Marker | Circle };
|
||||
|
||||
private _circles: Record<string, Circle> = {};
|
||||
|
||||
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 {
|
||||
if (!this.map.leafletMap || !this._locationMarkers) {
|
||||
if (!this._leafletMap || !this._locationMarkers) {
|
||||
return;
|
||||
}
|
||||
const marker = this._locationMarkers[id];
|
||||
@@ -88,44 +90,29 @@ export class HaLocationsEditor extends LitElement {
|
||||
return;
|
||||
}
|
||||
if ("getBounds" in marker) {
|
||||
this.map.leafletMap.fitBounds(marker.getBounds());
|
||||
this._leafletMap.fitBounds(marker.getBounds());
|
||||
(marker as Circle).bringToFront();
|
||||
} else {
|
||||
const circle = this._circles[id];
|
||||
if (circle) {
|
||||
this.map.leafletMap.fitBounds(circle.getBounds());
|
||||
this._leafletMap.fitBounds(circle.getBounds());
|
||||
} else {
|
||||
this.map.leafletMap.setView(marker.getLatLng(), this.zoom);
|
||||
this._leafletMap.setView(marker.getLatLng(), this.fitZoom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<ha-map
|
||||
.hass=${this.hass}
|
||||
.layers=${this._getLayers(this._circles, this._locationMarkers)}
|
||||
.zoom=${this.zoom}
|
||||
.autoFit=${this.autoFit}
|
||||
.darkMode=${this.darkMode}
|
||||
></ha-map>`;
|
||||
return html` <div id="map"></div> `;
|
||||
}
|
||||
|
||||
private _getLayers = memoizeOne(
|
||||
(
|
||||
circles: Record<string, Circle>,
|
||||
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;
|
||||
}
|
||||
);
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
this._initMap();
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
|
||||
// Still loading.
|
||||
if (!this.Leaflet) {
|
||||
@@ -135,6 +122,37 @@ export class HaLocationsEditor extends LitElement {
|
||||
if (changedProps.has("locations")) {
|
||||
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) {
|
||||
@@ -171,18 +189,21 @@ export class HaLocationsEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _updateMarkers(): void {
|
||||
if (!this.locations || !this.locations.length) {
|
||||
this._circles = {};
|
||||
if (this._locationMarkers) {
|
||||
Object.values(this._locationMarkers).forEach((marker) => {
|
||||
marker.remove();
|
||||
});
|
||||
this._locationMarkers = undefined;
|
||||
|
||||
Object.values(this._circles).forEach((circle) => circle.remove());
|
||||
this._circles = {};
|
||||
}
|
||||
|
||||
if (!this.locations || !this.locations.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const locationMarkers = {};
|
||||
const circles = {};
|
||||
|
||||
const defaultZoneRadiusColor = getComputedStyle(this).getPropertyValue(
|
||||
"--accent-color"
|
||||
);
|
||||
this._locationMarkers = {};
|
||||
|
||||
this.locations.forEach((location: MarkerLocation) => {
|
||||
let icon: DivIcon | undefined;
|
||||
@@ -207,46 +228,45 @@ export class HaLocationsEditor extends LitElement {
|
||||
const circle = this.Leaflet!.circle(
|
||||
[location.latitude, location.longitude],
|
||||
{
|
||||
color: location.radius_color || defaultZoneRadiusColor,
|
||||
color: location.radius_color || defaultRadiusColor,
|
||||
radius: location.radius,
|
||||
}
|
||||
);
|
||||
circle.addTo(this._leafletMap!);
|
||||
if (location.radius_editable || location.location_editable) {
|
||||
// @ts-ignore
|
||||
circle.editing.enable();
|
||||
circle.addEventListener("add", () => {
|
||||
// @ts-ignore
|
||||
const moveMarker = circle.editing._moveMarker;
|
||||
// @ts-ignore
|
||||
const resizeMarker = circle.editing._resizeMarkers[0];
|
||||
if (icon) {
|
||||
moveMarker.setIcon(icon);
|
||||
}
|
||||
resizeMarker.id = moveMarker.id = location.id;
|
||||
moveMarker
|
||||
.addEventListener(
|
||||
"dragend",
|
||||
// @ts-ignore
|
||||
(ev: DragEndEvent) => this._updateLocation(ev)
|
||||
)
|
||||
.addEventListener(
|
||||
"click",
|
||||
// @ts-ignore
|
||||
(ev: MouseEvent) => this._markerClicked(ev)
|
||||
);
|
||||
if (location.radius_editable) {
|
||||
resizeMarker.addEventListener(
|
||||
"dragend",
|
||||
// @ts-ignore
|
||||
(ev: DragEndEvent) => this._updateRadius(ev)
|
||||
);
|
||||
} else {
|
||||
resizeMarker.remove();
|
||||
}
|
||||
});
|
||||
locationMarkers[location.id] = circle;
|
||||
// @ts-ignore
|
||||
const moveMarker = circle.editing._moveMarker;
|
||||
// @ts-ignore
|
||||
const resizeMarker = circle.editing._resizeMarkers[0];
|
||||
if (icon) {
|
||||
moveMarker.setIcon(icon);
|
||||
}
|
||||
resizeMarker.id = moveMarker.id = location.id;
|
||||
moveMarker
|
||||
.addEventListener(
|
||||
"dragend",
|
||||
// @ts-ignore
|
||||
(ev: DragEndEvent) => this._updateLocation(ev)
|
||||
)
|
||||
.addEventListener(
|
||||
"click",
|
||||
// @ts-ignore
|
||||
(ev: MouseEvent) => this._markerClicked(ev)
|
||||
);
|
||||
if (location.radius_editable) {
|
||||
resizeMarker.addEventListener(
|
||||
"dragend",
|
||||
// @ts-ignore
|
||||
(ev: DragEndEvent) => this._updateRadius(ev)
|
||||
);
|
||||
} else {
|
||||
resizeMarker.remove();
|
||||
}
|
||||
this._locationMarkers![location.id] = circle;
|
||||
} else {
|
||||
circles[location.id] = circle;
|
||||
this._circles[location.id] = circle;
|
||||
}
|
||||
}
|
||||
if (
|
||||
@@ -255,7 +275,6 @@ export class HaLocationsEditor extends LitElement {
|
||||
) {
|
||||
const options: MarkerOptions = {
|
||||
title: location.name,
|
||||
draggable: location.location_editable,
|
||||
};
|
||||
|
||||
if (icon) {
|
||||
@@ -274,14 +293,13 @@ export class HaLocationsEditor extends LitElement {
|
||||
"click",
|
||||
// @ts-ignore
|
||||
(ev: MouseEvent) => this._markerClicked(ev)
|
||||
);
|
||||
)
|
||||
.addTo(this._leafletMap!);
|
||||
(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 {
|
||||
@@ -290,9 +308,23 @@ export class HaLocationsEditor extends LitElement {
|
||||
display: block;
|
||||
height: 300px;
|
||||
}
|
||||
ha-map {
|
||||
#map {
|
||||
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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -1,15 +1,13 @@
|
||||
import { Circle, Layer, Map, Marker, TileLayer } from "leaflet";
|
||||
import {
|
||||
Circle,
|
||||
CircleMarker,
|
||||
LatLngTuple,
|
||||
Layer,
|
||||
Map,
|
||||
Marker,
|
||||
Polyline,
|
||||
TileLayer,
|
||||
} from "leaflet";
|
||||
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import {
|
||||
LeafletModuleType,
|
||||
replaceTileLayer,
|
||||
@@ -17,324 +15,194 @@ import {
|
||||
} from "../../common/dom/setup-leaflet-map";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
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 "../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")
|
||||
export class HaMap extends ReactiveElement {
|
||||
class HaMap extends LitElement {
|
||||
@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({ 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;
|
||||
@property() public zoom?: number;
|
||||
|
||||
// eslint-disable-next-line
|
||||
private Leaflet?: LeafletModuleType;
|
||||
|
||||
private _leafletMap?: Map;
|
||||
|
||||
private _tileLayer?: TileLayer;
|
||||
|
||||
// @ts-ignore
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
private _debouncedResizeListener = debounce(
|
||||
() => {
|
||||
if (!this._leafletMap) {
|
||||
return;
|
||||
}
|
||||
this._leafletMap.invalidateSize();
|
||||
},
|
||||
100,
|
||||
false
|
||||
);
|
||||
|
||||
private _mapItems: Array<Marker | Circle> = [];
|
||||
|
||||
private _mapZones: Array<Marker | Circle> = [];
|
||||
|
||||
private _mapPaths: Array<Polyline | CircleMarker> = [];
|
||||
private _connected = false;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._loadMap();
|
||||
this._attachObserver();
|
||||
this._connected = true;
|
||||
if (this.hasUpdated) {
|
||||
this.loadMap();
|
||||
this._attachObserver();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (this.leafletMap) {
|
||||
this.leafletMap.remove();
|
||||
this.leafletMap = undefined;
|
||||
this._connected = false;
|
||||
|
||||
if (this._leafletMap) {
|
||||
this._leafletMap.remove();
|
||||
this._leafletMap = undefined;
|
||||
this.Leaflet = undefined;
|
||||
}
|
||||
|
||||
this._loaded = false;
|
||||
|
||||
if (this._resizeObserver) {
|
||||
this._resizeObserver.unobserve(this);
|
||||
this._resizeObserver.unobserve(this._mapEl);
|
||||
} else {
|
||||
window.removeEventListener("resize", this._debouncedResizeListener);
|
||||
}
|
||||
}
|
||||
|
||||
protected update(changedProps: PropertyValues) {
|
||||
super.update(changedProps);
|
||||
|
||||
if (!this._loaded) {
|
||||
return;
|
||||
protected render(): TemplateResult {
|
||||
if (!this.entities) {
|
||||
return html``;
|
||||
}
|
||||
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;
|
||||
|
||||
if (changedProps.has("_loaded") || changedProps.has("entities")) {
|
||||
this._drawEntities();
|
||||
} else if (this._loaded && oldHass && this.entities) {
|
||||
// Check if any state has changed
|
||||
for (const entity of this.entities) {
|
||||
if (
|
||||
oldHass.states[getEntityId(entity)] !==
|
||||
this.hass!.states[getEntityId(entity)]
|
||||
) {
|
||||
this._drawEntities();
|
||||
break;
|
||||
}
|
||||
if (!oldHass || !this.entities) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if any state has changed
|
||||
for (const entity of this.entities) {
|
||||
if (oldHass.states[entity] !== this.hass!.states[entity]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("_loaded") || changedProps.has("paths")) {
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
|
||||
private async _loadMap(): Promise<void> {
|
||||
let map = this.shadowRoot!.getElementById("map");
|
||||
if (!map) {
|
||||
map = document.createElement("div");
|
||||
map.id = "map";
|
||||
this.shadowRoot!.append(map);
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
if (changedProps.has("hass")) {
|
||||
this._drawEntities();
|
||||
this._fitMap();
|
||||
|
||||
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 {
|
||||
if (!this.leafletMap || !this.Leaflet || !this.hass) {
|
||||
private get _mapEl(): HTMLDivElement {
|
||||
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;
|
||||
}
|
||||
|
||||
if (!this._mapItems.length && !this.layers?.length) {
|
||||
this.leafletMap.setView(
|
||||
if (this._mapItems.length === 0) {
|
||||
this._leafletMap.setView(
|
||||
new this.Leaflet.LatLng(
|
||||
this.hass.config.latitude,
|
||||
this.hass.config.longitude
|
||||
),
|
||||
this.zoom
|
||||
this.zoom || 14
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let bounds = this.Leaflet.latLngBounds(
|
||||
const bounds = this.Leaflet.latLngBounds(
|
||||
this._mapItems ? this._mapItems.map((item) => item.getLatLng()) : []
|
||||
);
|
||||
this._leafletMap.fitBounds(bounds.pad(0.5));
|
||||
|
||||
if (this.fitZones) {
|
||||
this._mapZones?.forEach((zone) => {
|
||||
bounds.extend(
|
||||
"getBounds" in zone ? zone.getBounds() : zone.getLatLng()
|
||||
);
|
||||
});
|
||||
if (this.zoom && this._leafletMap.getZoom() > this.zoom) {
|
||||
this._leafletMap.setZoom(this.zoom);
|
||||
}
|
||||
|
||||
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 {
|
||||
const hass = this.hass;
|
||||
const map = this.leafletMap;
|
||||
const map = this._leafletMap;
|
||||
const Leaflet = this.Leaflet;
|
||||
|
||||
if (!hass || !map || !Leaflet) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._mapItems.length) {
|
||||
if (this._mapItems) {
|
||||
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 = [];
|
||||
}
|
||||
const mapZones: Layer[] = (this._mapZones = []);
|
||||
|
||||
if (!this.entities) {
|
||||
return;
|
||||
}
|
||||
const allEntities = this.entities!.concat();
|
||||
|
||||
const computedStyles = getComputedStyle(this);
|
||||
const zoneColor = computedStyles.getPropertyValue("--accent-color");
|
||||
const darkPrimaryColor = computedStyles.getPropertyValue(
|
||||
"--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)];
|
||||
for (const entity of allEntities) {
|
||||
const entityId = entity;
|
||||
const stateObj = hass.states[entityId];
|
||||
if (!stateObj) {
|
||||
continue;
|
||||
}
|
||||
@@ -372,12 +240,13 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
|
||||
// create marker with the icon
|
||||
this._mapZones.push(
|
||||
mapZones.push(
|
||||
Leaflet.marker([latitude, longitude], {
|
||||
icon: Leaflet.divIcon({
|
||||
html: iconHTML,
|
||||
iconSize: [24, 24],
|
||||
className,
|
||||
className:
|
||||
this.darkMode ?? this.hass.themes.darkMode ? "dark" : "light",
|
||||
}),
|
||||
interactive: false,
|
||||
title,
|
||||
@@ -385,10 +254,10 @@ export class HaMap extends ReactiveElement {
|
||||
);
|
||||
|
||||
// create circle around it
|
||||
this._mapZones.push(
|
||||
mapZones.push(
|
||||
Leaflet.circle([latitude, longitude], {
|
||||
interactive: false,
|
||||
color: zoneColor,
|
||||
color: "#FF9800",
|
||||
radius,
|
||||
})
|
||||
);
|
||||
@@ -404,20 +273,17 @@ export class HaMap extends ReactiveElement {
|
||||
.join("")
|
||||
.substr(0, 3);
|
||||
|
||||
// create marker with the icon
|
||||
this._mapItems.push(
|
||||
// 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="${getEntityId(entity)}"
|
||||
entity-id="${entityId}"
|
||||
entity-name="${entityName}"
|
||||
entity-picture="${entityPicture || ""}"
|
||||
${
|
||||
typeof entity !== "string"
|
||||
? `entity-color="${entity.color}"`
|
||||
: ""
|
||||
}
|
||||
></ha-entity-marker>
|
||||
`,
|
||||
iconSize: [48, 48],
|
||||
@@ -429,10 +295,10 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
// create circle around if entity has accuracy
|
||||
if (gpsAccuracy) {
|
||||
this._mapItems.push(
|
||||
mapItems.push(
|
||||
Leaflet.circle([latitude, longitude], {
|
||||
interactive: false,
|
||||
color: darkPrimaryColor,
|
||||
color: "#0288D1",
|
||||
radius: gpsAccuracy,
|
||||
})
|
||||
);
|
||||
@@ -443,14 +309,20 @@ export class HaMap extends ReactiveElement {
|
||||
this._mapZones.forEach((marker) => map.addLayer(marker));
|
||||
}
|
||||
|
||||
private async _attachObserver(): Promise<void> {
|
||||
if (!this._resizeObserver) {
|
||||
await installResizeObserver();
|
||||
this._resizeObserver = new ResizeObserver(() => {
|
||||
this.leafletMap?.invalidateSize({ debounceMoveend: true });
|
||||
});
|
||||
private _attachObserver(): void {
|
||||
// Observe changes to map size and invalidate to prevent broken rendering
|
||||
// Uses ResizeObserver in Chrome, otherwise window resize event
|
||||
|
||||
// @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 {
|
||||
@@ -465,25 +337,13 @@ export class HaMap extends ReactiveElement {
|
||||
#map.dark {
|
||||
background: #090909;
|
||||
}
|
||||
.light {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.dark {
|
||||
color: #ffffff;
|
||||
}
|
||||
.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;
|
||||
|
||||
.light {
|
||||
color: #000000;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -45,6 +45,7 @@ import "../ha-button-menu";
|
||||
import "../ha-card";
|
||||
import "../ha-circular-progress";
|
||||
import "../ha-fab";
|
||||
import "../ha-paper-dropdown-menu";
|
||||
import "../ha-svg-icon";
|
||||
|
||||
declare global {
|
||||
|
@@ -377,10 +377,7 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
|
||||
major: {
|
||||
fontStyle: "bold",
|
||||
},
|
||||
source: "auto",
|
||||
sampleSize: 5,
|
||||
autoSkipPadding: 20,
|
||||
maxRotation: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@@ -236,9 +236,7 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
|
||||
major: {
|
||||
fontStyle: "bold",
|
||||
},
|
||||
sampleSize: 5,
|
||||
autoSkipPadding: 50,
|
||||
maxRotation: 0,
|
||||
},
|
||||
categoryPercentage: undefined,
|
||||
barPercentage: undefined,
|
||||
|
@@ -12,20 +12,20 @@ export interface ConfigEntry {
|
||||
| "setup_retry"
|
||||
| "not_loaded"
|
||||
| "failed_unload";
|
||||
connection_class: string;
|
||||
supports_options: boolean;
|
||||
supports_unload: boolean;
|
||||
pref_disable_new_entities: boolean;
|
||||
pref_disable_polling: boolean;
|
||||
disabled_by: "user" | null;
|
||||
reason: string | null;
|
||||
}
|
||||
|
||||
export type ConfigEntryMutableParams = Partial<
|
||||
Pick<
|
||||
ConfigEntry,
|
||||
"title" | "pref_disable_new_entities" | "pref_disable_polling"
|
||||
>
|
||||
>;
|
||||
export interface ConfigEntryMutableParams {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface ConfigEntrySystemOptions {
|
||||
disable_new_entities: boolean;
|
||||
}
|
||||
|
||||
export const getConfigEntries = (hass: HomeAssistant) =>
|
||||
hass.callApi<ConfigEntry[]>("GET", "config/config_entries/entry");
|
||||
@@ -33,9 +33,9 @@ export const getConfigEntries = (hass: HomeAssistant) =>
|
||||
export const updateConfigEntry = (
|
||||
hass: HomeAssistant,
|
||||
configEntryId: string,
|
||||
updatedValues: ConfigEntryMutableParams
|
||||
updatedValues: Partial<ConfigEntryMutableParams>
|
||||
) =>
|
||||
hass.callWS<{ require_restart: boolean; config_entry: ConfigEntry }>({
|
||||
hass.callWS<ConfigEntry>({
|
||||
type: "config_entries/update",
|
||||
entry_id: configEntryId,
|
||||
...updatedValues,
|
||||
@@ -51,15 +51,13 @@ export const reloadConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
|
||||
require_restart: boolean;
|
||||
}>("POST", `config/config_entries/entry/${configEntryId}/reload`);
|
||||
|
||||
export interface DisableConfigEntryResult {
|
||||
require_restart: boolean;
|
||||
}
|
||||
|
||||
export const disableConfigEntry = (
|
||||
hass: HomeAssistant,
|
||||
configEntryId: string
|
||||
) =>
|
||||
hass.callWS<DisableConfigEntryResult>({
|
||||
hass.callWS<{
|
||||
require_restart: boolean;
|
||||
}>({
|
||||
type: "config_entries/disable",
|
||||
entry_id: configEntryId,
|
||||
disabled_by: "user",
|
||||
@@ -73,3 +71,23 @@ export const enableConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
|
||||
entry_id: configEntryId,
|
||||
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,
|
||||
});
|
||||
|
@@ -212,15 +212,13 @@ export const setHassioAddonOption = async (
|
||||
|
||||
export const validateHassioAddonOption = async (
|
||||
hass: HomeAssistant,
|
||||
slug: string,
|
||||
data?: any
|
||||
slug: string
|
||||
): Promise<{ message: string; valid: boolean }> => {
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
return hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/options/validate`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -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 {
|
||||
devices: HardwareDevice[];
|
||||
serial: string[];
|
||||
input: string[];
|
||||
disk: string[];
|
||||
gpio: string[];
|
||||
audio: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export const fetchHassioHardwareAudio = async (
|
||||
|
@@ -41,7 +41,6 @@ export interface HassioSnapshotDetail extends HassioSnapshot {
|
||||
export interface HassioFullSnapshotCreateParams {
|
||||
name: string;
|
||||
password?: string;
|
||||
confirm_password?: string;
|
||||
}
|
||||
export interface HassioPartialSnapshotCreateParams
|
||||
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 (
|
||||
hass: HomeAssistant,
|
||||
data: HassioPartialSnapshotCreateParams
|
||||
|
@@ -36,16 +36,18 @@ export const getIcon = (iconName: string) =>
|
||||
.then((icon) => resolve_(icon))
|
||||
.catch((e) => reject_(e));
|
||||
}
|
||||
toRead = [];
|
||||
})
|
||||
).catch((e) => {
|
||||
// Firefox in private mode doesn't support IDB
|
||||
// Safari sometime doesn't open the DB so we time out
|
||||
for (const [, , reject_] of toRead) {
|
||||
reject_(e);
|
||||
}
|
||||
toRead = [];
|
||||
});
|
||||
)
|
||||
.catch((e) => {
|
||||
// Firefox in private mode doesn't support IDB
|
||||
// Safari sometime doesn't open the DB so we time out
|
||||
for (const [, , reject_] of toRead) {
|
||||
reject_(e);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
toRead = [];
|
||||
});
|
||||
});
|
||||
|
||||
export const findIconChunk = (icon: string): string => {
|
||||
|
@@ -2,7 +2,6 @@ import { Connection, getCollection } from "home-assistant-js-websocket";
|
||||
import { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { fetchFrontendUserData, saveFrontendUserData } from "../frontend";
|
||||
import { HassioAddonsInfo } from "../hassio/addon";
|
||||
import { HassioHassOSInfo, HassioHostInfo } from "../hassio/host";
|
||||
import { NetworkInfo } from "../hassio/network";
|
||||
@@ -14,28 +13,6 @@ import {
|
||||
} from "../hassio/supervisor";
|
||||
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 = {
|
||||
type: "supervisor/api",
|
||||
method: "GET",
|
||||
|
@@ -1,6 +1,14 @@
|
||||
import { navigate } from "../common/navigate";
|
||||
import {
|
||||
DEFAULT_ACCENT_COLOR,
|
||||
DEFAULT_PRIMARY_COLOR,
|
||||
} from "../resources/ha-style";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export const defaultRadiusColor = DEFAULT_ACCENT_COLOR;
|
||||
export const homeRadiusColor = DEFAULT_PRIMARY_COLOR;
|
||||
export const passiveRadiusColor = "#9b9b9b";
|
||||
|
||||
export interface Zone {
|
||||
id: string;
|
||||
name: string;
|
||||
|
@@ -3,17 +3,17 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-formfield";
|
||||
import "../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../components/ha-switch";
|
||||
import {
|
||||
ConfigEntryMutableParams,
|
||||
updateConfigEntry,
|
||||
getConfigEntrySystemOptions,
|
||||
updateConfigEntrySystemOptions,
|
||||
} from "../../data/config_entries";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showAlertDialog } from "../generic/show-dialog-box";
|
||||
import { ConfigEntrySystemOptionsDialogParams } from "./show-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 _disablePolling!: boolean;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _params?: ConfigEntrySystemOptionsDialogParams;
|
||||
|
||||
@state() private _loading = false;
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
public async showDialog(
|
||||
@@ -35,8 +35,13 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
this._disableNewEntities = params.entry.pref_disable_new_entities;
|
||||
this._disablePolling = params.entry.pref_disable_polling;
|
||||
this._loading = true;
|
||||
const systemOptions = await getConfigEntrySystemOptions(
|
||||
this.hass,
|
||||
params.entry.entry_id
|
||||
);
|
||||
this._loading = false;
|
||||
this._disableNewEntities = systemOptions.disable_new_entities;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
@@ -61,57 +66,45 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
this._params.entry.domain
|
||||
)}
|
||||
>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
<ha-formfield
|
||||
.label=${html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.config_entry_system_options.enable_new_entities_label"
|
||||
)}
|
||||
</p>
|
||||
<p class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.config_entry_system_options.enable_new_entities_description",
|
||||
"integration",
|
||||
this.hass.localize(
|
||||
`component.${this._params.entry.domain}.title`
|
||||
) || this._params.entry.domain
|
||||
)}
|
||||
</p>`}
|
||||
.dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${!this._disableNewEntities}
|
||||
@change=${this._disableNewEntitiesChanged}
|
||||
.disabled=${this._submitting}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
${this._allowUpdatePolling()
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.config_entry_system_options.enable_polling_label"
|
||||
)}
|
||||
</p>
|
||||
<p class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.config_entry_system_options.enable_polling_description",
|
||||
"integration",
|
||||
this.hass.localize(
|
||||
`component.${this._params.entry.domain}.title`
|
||||
) || this._params.entry.domain
|
||||
)}
|
||||
</p>`}
|
||||
.dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${!this._disablePolling}
|
||||
@change=${this._disablePollingChanged}
|
||||
.disabled=${this._submitting}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
`
|
||||
: ""}
|
||||
<div>
|
||||
${this._loading
|
||||
? html`
|
||||
<div class="init-spinner">
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
${this._error
|
||||
? html` <div class="error">${this._error}</div> `
|
||||
: ""}
|
||||
<div class="form">
|
||||
<ha-formfield
|
||||
.label=${html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.config_entry_system_options.enable_new_entities_label"
|
||||
)}
|
||||
</p>
|
||||
<p class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.config_entry_system_options.enable_new_entities_description",
|
||||
"integration",
|
||||
this.hass.localize(
|
||||
`component.${this._params.entry.domain}.title`
|
||||
) || this._params.entry.domain
|
||||
)}
|
||||
</p>`}
|
||||
.dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${!this._disableNewEntities}
|
||||
@change=${this._disableNewEntitiesChanged}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
@@ -122,7 +115,7 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click="${this._updateEntry}"
|
||||
.disabled=${this._submitting}
|
||||
.disabled=${this._submitting || this._loading}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.config_entry_system_options.update")}
|
||||
</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 {
|
||||
this._error = undefined;
|
||||
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> {
|
||||
this._submitting = true;
|
||||
const data: ConfigEntryMutableParams = {
|
||||
pref_disable_new_entities: this._disableNewEntities,
|
||||
};
|
||||
if (this._allowUpdatePolling()) {
|
||||
data.pref_disable_polling = this._disablePolling;
|
||||
}
|
||||
try {
|
||||
const result = await updateConfigEntry(
|
||||
await updateConfigEntrySystemOptions(
|
||||
this.hass,
|
||||
this._params!.entry.entry_id,
|
||||
data
|
||||
{
|
||||
disable_new_entities: this._disableNewEntities,
|
||||
}
|
||||
);
|
||||
if (result.require_restart) {
|
||||
await showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.dialogs.config_entry_system_options.restart_home_assistant"
|
||||
),
|
||||
});
|
||||
}
|
||||
this._params!.entryUpdated(result.config_entry);
|
||||
this.closeDialog();
|
||||
this._params = undefined;
|
||||
} catch (err) {
|
||||
this._error = err.message || "Unknown error";
|
||||
} finally {
|
||||
@@ -182,6 +150,20 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
return [
|
||||
haStyleDialog,
|
||||
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 {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { ConfigEntry } from "../../data/config_entries";
|
||||
import { IntegrationManifest } from "../../data/integration";
|
||||
|
||||
export interface ConfigEntrySystemOptionsDialogParams {
|
||||
entry: ConfigEntry;
|
||||
manifest?: IntegrationManifest;
|
||||
entryUpdated(entry: ConfigEntry): void;
|
||||
// updateEntry: (
|
||||
// updates: Partial<EntityRegistryEntryUpdateParams>
|
||||
// ) => Promise<unknown>;
|
||||
// removeEntry: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const loadConfigEntrySystemOptionsDialog = () =>
|
||||
|
@@ -108,28 +108,12 @@ export const showOptionsFlowDialog = (
|
||||
`;
|
||||
},
|
||||
|
||||
renderShowFormProgressHeader(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.options.step.${step.step_id}.title`
|
||||
) || hass.localize(`component.${configEntry.domain}.title`)
|
||||
);
|
||||
renderShowFormProgressHeader(_hass, _step) {
|
||||
return "";
|
||||
},
|
||||
|
||||
renderShowFormProgressDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${configEntry.domain}.options.progress.${step.progress_action}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
return description
|
||||
? html`
|
||||
<ha-markdown
|
||||
allowsvg
|
||||
breaks
|
||||
.content=${description}
|
||||
></ha-markdown>
|
||||
`
|
||||
: "";
|
||||
renderShowFormProgressDescription(_hass, _step) {
|
||||
return "";
|
||||
},
|
||||
}
|
||||
);
|
||||
|
74
src/dialogs/ha-store-auth-card.js
Normal file
74
src/dialogs/ha-store-auth-card.js
Normal 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);
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -23,12 +23,16 @@ class MoreInfoPerson extends LitElement {
|
||||
}
|
||||
|
||||
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
|
||||
? html`
|
||||
<ha-map
|
||||
.hass=${this.hass}
|
||||
.entities=${this._entityArray(this.stateObj.entity_id)}
|
||||
autoFit
|
||||
></ha-map>
|
||||
`
|
||||
: ""}
|
||||
@@ -47,11 +51,6 @@ class MoreInfoPerson extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<ha-attributes
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
extra-filters="id,user_id,editable"
|
||||
></ha-attributes>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -17,6 +17,11 @@ class MoreInfoTimer extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-attributes
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
extra-filters="remaining"
|
||||
></ha-attributes>
|
||||
<div class="actions">
|
||||
${this.stateObj.state === "idle" || this.stateObj.state === "paused"
|
||||
? html`
|
||||
@@ -52,11 +57,6 @@ class MoreInfoTimer extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<ha-attributes
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
extra-filters="remaining"
|
||||
></ha-attributes>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -108,7 +108,7 @@ class MoreInfoWeather extends LitElement {
|
||||
this.stateObj.attributes.pressure,
|
||||
this.hass.locale
|
||||
)}
|
||||
${getWeatherUnit(this.hass, "pressure")}
|
||||
${getWeatherUnit(this.hass, "air_pressure")}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
@@ -5,7 +5,6 @@ import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { throttle } from "../../common/util/throttle";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/state-history-charts";
|
||||
import { fetchUsers } from "../../data/user";
|
||||
import { getLogbookData, LogbookEntry } from "../../data/logbook";
|
||||
import { loadTraceContexts, TraceContexts } from "../../data/trace";
|
||||
import "../../panels/logbook/ha-logbook";
|
||||
@@ -23,12 +22,10 @@ export class MoreInfoLogbook extends LitElement {
|
||||
|
||||
@state() private _traceContexts?: TraceContexts;
|
||||
|
||||
@state() private _userIdToName = {};
|
||||
@state() private _persons = {};
|
||||
|
||||
private _lastLogbookDate?: Date;
|
||||
|
||||
private _fetchUserPromise?: Promise<void>;
|
||||
|
||||
private _throttleGetLogbookEntries = throttle(() => {
|
||||
this._getLogBookData();
|
||||
}, 10000);
|
||||
@@ -62,7 +59,7 @@ export class MoreInfoLogbook extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.entries=${this._logbookEntries}
|
||||
.traceContexts=${this._traceContexts}
|
||||
.userIdToName=${this._userIdToName}
|
||||
.userIdToName=${this._persons}
|
||||
></ha-logbook>
|
||||
`
|
||||
: html`<div class="no-entries">
|
||||
@@ -73,7 +70,7 @@ export class MoreInfoLogbook extends LitElement {
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._fetchUserPromise = this._fetchUserNames();
|
||||
this._fetchPersonNames();
|
||||
this.addEventListener("click", (ev) => {
|
||||
if ((ev.composedPath()[0] as HTMLElement).tagName === "A") {
|
||||
setTimeout(() => closeDialog("ha-more-info-dialog"), 500);
|
||||
@@ -128,7 +125,6 @@ export class MoreInfoLogbook extends LitElement {
|
||||
true
|
||||
),
|
||||
loadTraceContexts(this.hass),
|
||||
this._fetchUserPromise,
|
||||
]);
|
||||
this._logbookEntries = this._logbookEntries
|
||||
? [...newEntries, ...this._logbookEntries]
|
||||
@@ -137,34 +133,16 @@ export class MoreInfoLogbook extends LitElement {
|
||||
this._traceContexts = traceContexts;
|
||||
}
|
||||
|
||||
private async _fetchUserNames() {
|
||||
const userIdToName = {};
|
||||
|
||||
// Start loading users
|
||||
const userProm = this.hass.user?.is_admin && fetchUsers(this.hass);
|
||||
|
||||
// Process persons
|
||||
private _fetchPersonNames() {
|
||||
Object.values(this.hass.states).forEach((entity) => {
|
||||
if (
|
||||
entity.attributes.user_id &&
|
||||
computeStateDomain(entity) === "person"
|
||||
) {
|
||||
this._userIdToName[entity.attributes.user_id] =
|
||||
this._persons[entity.attributes.user_id] =
|
||||
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() {
|
||||
|
@@ -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 type { List } from "@material/mwc-list/mwc-list";
|
||||
import { SingleSelectedEvent } from "@material/mwc-list/mwc-list-foundation";
|
||||
@@ -188,6 +188,7 @@ export class QuickBar extends LitElement {
|
||||
${scroll({
|
||||
items,
|
||||
layout: Layout1d,
|
||||
// @ts-expect-error
|
||||
renderItem: (item: QuickBarItem, index) =>
|
||||
this._renderItem(item, index),
|
||||
})}
|
||||
@@ -223,7 +224,7 @@ export class QuickBar extends LitElement {
|
||||
|
||||
private _renderItem(item: QuickBarItem, index?: number) {
|
||||
if (!item) {
|
||||
return html``;
|
||||
return undefined;
|
||||
}
|
||||
return isCommandItem(item)
|
||||
? this._renderCommandItem(item, index)
|
||||
@@ -638,6 +639,18 @@ export class QuickBar extends LitElement {
|
||||
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 {
|
||||
width: 100%;
|
||||
}
|
||||
|
@@ -48,9 +48,6 @@
|
||||
window.providersPromise = fetch("/auth/providers", {
|
||||
credentials: "same-origin",
|
||||
});
|
||||
if (!window.globalThis) {
|
||||
window.globalThis = window;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
|
@@ -71,9 +71,6 @@
|
||||
import("<%= latestAppJS %>");
|
||||
window.customPanelJS = "<%= latestCustomPanelJS %>";
|
||||
window.latestJS = true;
|
||||
if (!window.globalThis) {
|
||||
window.globalThis = window;
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
{% for extra_module in extra_modules -%}
|
||||
|
@@ -80,9 +80,6 @@
|
||||
window.stepsPromise = fetch("/api/onboarding", {
|
||||
credentials: "same-origin",
|
||||
});
|
||||
if (!window.globalThis) {
|
||||
window.globalThis = window;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
|
@@ -229,7 +229,7 @@ class HassTabsSubpage extends LitElement {
|
||||
color: var(--sidebar-text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
.bottom-bar a {
|
||||
:host([narrow]) .toolbar a {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
|
@@ -12,10 +12,7 @@ import { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||
import { subscribeOne } from "../common/util/subscribe-one";
|
||||
import { AuthUrlSearchParams, hassUrl } from "../data/auth";
|
||||
import {
|
||||
DiscoveryInformation,
|
||||
fetchDiscoveryInformation,
|
||||
} from "../data/discovery";
|
||||
import { fetchDiscoveryInformation } from "../data/discovery";
|
||||
import {
|
||||
fetchOnboardingOverview,
|
||||
OnboardingResponses,
|
||||
@@ -71,8 +68,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
|
||||
@state() private _steps?: OnboardingStep[];
|
||||
|
||||
@state() private _discoveryInformation?: DiscoveryInformation;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const step = this._curStep()!;
|
||||
|
||||
@@ -92,7 +87,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
? html`<onboarding-restore-snapshot
|
||||
.localize=${this.localize}
|
||||
.restoring=${this._restoring}
|
||||
.discoveryInformation=${this._discoveryInformation}
|
||||
@restoring=${this._restoringSnapshot}
|
||||
>
|
||||
</onboarding-restore-snapshot>`
|
||||
|
@@ -5,11 +5,9 @@ import "@polymer/paper-radio-button/paper-radio-button";
|
||||
import "@polymer/paper-radio-group/paper-radio-group";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/map/ha-locations-editor";
|
||||
import type { MarkerLocation } from "../components/map/ha-locations-editor";
|
||||
import "../components/map/ha-location-editor";
|
||||
import { createTimezoneListEl } from "../components/timezone-datalist";
|
||||
import {
|
||||
ConfigUpdateValues,
|
||||
@@ -83,14 +81,14 @@ class OnboardingCoreConfig extends LitElement {
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<ha-locations-editor
|
||||
<ha-location-editor
|
||||
class="flex"
|
||||
.hass=${this.hass}
|
||||
.locations=${this._markerLocation(this._locationValue)}
|
||||
zoom="14"
|
||||
.location=${this._locationValue}
|
||||
.fitZoom=${14}
|
||||
.darkMode=${mql.matches}
|
||||
@location-updated=${this._locationChanged}
|
||||
></ha-locations-editor>
|
||||
@change=${this._locationChanged}
|
||||
></ha-location-editor>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
@@ -210,24 +208,13 @@ class OnboardingCoreConfig extends LitElement {
|
||||
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>) {
|
||||
const target = ev.currentTarget as PaperInputElement;
|
||||
this[`_${target.name}`] = target.value;
|
||||
}
|
||||
|
||||
private _locationChanged(ev) {
|
||||
this._location = ev.detail.location;
|
||||
this._location = ev.currentTarget.location;
|
||||
}
|
||||
|
||||
private _unitSystemChanged(
|
||||
|
@@ -4,12 +4,9 @@ import { customElement, property } from "lit/decorators";
|
||||
import "../../hassio/src/components/hassio-ansi-to-html";
|
||||
import { showHassioSnapshotDialog } from "../../hassio/src/dialogs/snapshot/show-dialog-hassio-snapshot";
|
||||
import { showSnapshotUploadDialog } from "../../hassio/src/dialogs/snapshot/show-dialog-snapshot-upload";
|
||||
import { navigate } from "../common/navigate";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-card";
|
||||
import {
|
||||
DiscoveryInformation,
|
||||
fetchDiscoveryInformation,
|
||||
} from "../data/discovery";
|
||||
import { makeDialogManager } from "../dialogs/make-dialog-manager";
|
||||
import { ProvideHassLitMixin } from "../mixins/provide-hass-lit-mixin";
|
||||
import { haStyle } from "../resources/styles";
|
||||
@@ -29,9 +26,6 @@ class OnboardingRestoreSnapshot extends ProvideHassLitMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean }) public restoring = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public discoveryInformation?: DiscoveryInformation;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return this.restoring
|
||||
? html`<ha-card
|
||||
@@ -64,14 +58,13 @@ class OnboardingRestoreSnapshot extends ProvideHassLitMixin(LitElement) {
|
||||
private async _checkRestoreStatus(): Promise<void> {
|
||||
if (this.restoring) {
|
||||
try {
|
||||
const response = await fetchDiscoveryInformation();
|
||||
|
||||
if (
|
||||
!this.discoveryInformation ||
|
||||
this.discoveryInformation.uuid !== response.uuid
|
||||
) {
|
||||
// When the UUID changes, the restore is complete
|
||||
window.location.replace("/");
|
||||
const response = await fetch("/api/hassio/supervisor/info", {
|
||||
method: "GET",
|
||||
});
|
||||
if (response.status === 401) {
|
||||
// If we get a unauthorized response, the restore is done
|
||||
navigate("/", { replace: true });
|
||||
location.reload();
|
||||
}
|
||||
} catch (err) {
|
||||
// We fully expected issues with fetching info untill restore is complete.
|
||||
@@ -83,7 +76,6 @@ class OnboardingRestoreSnapshot extends ProvideHassLitMixin(LitElement) {
|
||||
showHassioSnapshotDialog(this, {
|
||||
slug,
|
||||
onboarding: true,
|
||||
localize: this.localize,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -6,7 +6,7 @@ import { TagTrigger } from "../../../../../data/automation";
|
||||
import { fetchTags, Tag } from "../../../../../data/tag";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { TriggerElement } from "../ha-automation-trigger-row";
|
||||
import "../../../../../components/ha-paper-dropdown-menu";
|
||||
|
||||
@customElement("ha-automation-trigger-tag")
|
||||
export class HaTagTrigger extends LitElement implements TriggerElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
@@ -8,8 +8,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { UNIT_C } from "../../../common/const";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/map/ha-locations-editor";
|
||||
import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
|
||||
import "../../../components/map/ha-location-editor";
|
||||
import { createTimezoneListEl } from "../../../components/timezone-datalist";
|
||||
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
|
||||
import type { PolymerChangedEvent } from "../../../polymer-types";
|
||||
@@ -21,13 +20,13 @@ class ConfigCoreForm extends LitElement {
|
||||
|
||||
@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 {
|
||||
const canEdit = ["storage", "default"].includes(
|
||||
@@ -53,16 +52,16 @@ class ConfigCoreForm extends LitElement {
|
||||
: ""}
|
||||
|
||||
<div class="row">
|
||||
<ha-locations-editor
|
||||
<ha-location-editor
|
||||
class="flex"
|
||||
.hass=${this.hass}
|
||||
.locations=${this._markerLocation(
|
||||
.location=${this._locationValue(
|
||||
this._location,
|
||||
this.hass.config.latitude,
|
||||
this.hass.config.longitude,
|
||||
this._location
|
||||
this.hass.config.longitude
|
||||
)}
|
||||
@location-updated=${this._locationChanged}
|
||||
></ha-locations-editor>
|
||||
@change=${this._locationChanged}
|
||||
></ha-location-editor>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
@@ -163,19 +162,8 @@ class ConfigCoreForm extends LitElement {
|
||||
input.inputElement.appendChild(createTimezoneListEl());
|
||||
}
|
||||
|
||||
private _markerLocation = memoizeOne(
|
||||
(
|
||||
lat: number,
|
||||
lng: number,
|
||||
location?: [number, number]
|
||||
): MarkerLocation[] => [
|
||||
{
|
||||
id: "location",
|
||||
latitude: location ? location[0] : lat,
|
||||
longitude: location ? location[1] : lng,
|
||||
location_editable: true,
|
||||
},
|
||||
]
|
||||
private _locationValue = memoizeOne(
|
||||
(location, lat, lng) => location || [Number(lat), Number(lng)]
|
||||
);
|
||||
|
||||
private get _elevationValue() {
|
||||
@@ -204,7 +192,7 @@ class ConfigCoreForm extends LitElement {
|
||||
}
|
||||
|
||||
private _locationChanged(ev) {
|
||||
this._location = ev.detail.location;
|
||||
this._location = ev.currentTarget.location;
|
||||
}
|
||||
|
||||
private _unitSystemChanged(
|
||||
@@ -216,10 +204,11 @@ class ConfigCoreForm extends LitElement {
|
||||
private async _save() {
|
||||
this._working = true;
|
||||
try {
|
||||
const location = this._location || [
|
||||
const location = this._locationValue(
|
||||
this._location,
|
||||
this.hass.config.latitude,
|
||||
this.hass.config.longitude,
|
||||
];
|
||||
this.hass.config.longitude
|
||||
);
|
||||
await saveCoreConfig(this.hass, {
|
||||
latitude: location[0],
|
||||
longitude: location[1],
|
||||
|
@@ -9,14 +9,13 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/ha-network";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-network";
|
||||
import "../../../components/ha-settings-row";
|
||||
import { fetchNetworkInfo } from "../../../data/hassio/network";
|
||||
import {
|
||||
getNetworkConfig,
|
||||
NetworkConfig,
|
||||
getNetworkConfig,
|
||||
setNetworkConfig,
|
||||
} from "../../../data/network";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
@@ -74,19 +73,7 @@ class ConfigNetwork extends LitElement {
|
||||
private async _load() {
|
||||
this._error = undefined;
|
||||
try {
|
||||
const coreNetwork = 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;
|
||||
this._networkConfig = await getNetworkConfig(this.hass);
|
||||
} catch (err) {
|
||||
this._error = err.message || err;
|
||||
}
|
||||
|
@@ -11,11 +11,7 @@ import { slugify } from "../../../common/string/slugify";
|
||||
import "../../../components/entity/ha-battery-icon";
|
||||
import "../../../components/ha-icon-next";
|
||||
import { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import {
|
||||
ConfigEntry,
|
||||
disableConfigEntry,
|
||||
DisableConfigEntryResult,
|
||||
} from "../../../data/config_entries";
|
||||
import { ConfigEntry, disableConfigEntry } from "../../../data/config_entries";
|
||||
import {
|
||||
computeDeviceName,
|
||||
DeviceRegistryEntry,
|
||||
@@ -29,10 +25,7 @@ import {
|
||||
} from "../../../data/entity_registry";
|
||||
import { SceneEntities, showSceneEditor } from "../../../data/scene";
|
||||
import { findRelated, RelatedResult } from "../../../data/search";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-error-screen";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
@@ -678,41 +671,13 @@ export class HaConfigDevicePage extends LitElement {
|
||||
dismissText: this.hass.localize("ui.common.no"),
|
||||
}))
|
||||
) {
|
||||
let result: DisableConfigEntryResult;
|
||||
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"
|
||||
),
|
||||
});
|
||||
}
|
||||
disableConfigEntry(this.hass, cnfg_entry);
|
||||
delete updates.disabled_by;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
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,
|
||||
});
|
||||
}
|
||||
await updateDeviceRegistryEntry(this.hass, this.deviceId, updates);
|
||||
|
||||
if (
|
||||
!oldDeviceName ||
|
||||
|
@@ -193,6 +193,9 @@ class HaInputNumberForm extends LitElement {
|
||||
.form {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-paper-dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -196,6 +196,9 @@ class HaInputSelectForm extends LitElement {
|
||||
mwc-button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
ha-paper-dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -179,6 +179,9 @@ class HaInputTextForm extends LitElement {
|
||||
.row {
|
||||
padding: 16px 0;
|
||||
}
|
||||
ha-paper-dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -18,7 +18,6 @@ import {
|
||||
ConfigEntry,
|
||||
deleteConfigEntry,
|
||||
disableConfigEntry,
|
||||
DisableConfigEntryResult,
|
||||
enableConfigEntry,
|
||||
reloadConfigEntry,
|
||||
updateConfigEntry,
|
||||
@@ -111,7 +110,6 @@ export class HaIntegrationCard extends LitElement {
|
||||
: undefined}
|
||||
.localizedDomainName=${item ? item.localized_domain_name : undefined}
|
||||
.manifest=${this.manifest}
|
||||
.configEntry=${item}
|
||||
>
|
||||
${this.items.length > 1
|
||||
? html`
|
||||
@@ -468,11 +466,6 @@ export class HaIntegrationCard extends LitElement {
|
||||
private _showSystemOptions(configEntry: ConfigEntry) {
|
||||
showConfigEntrySystemOptionsDialog(this, {
|
||||
entry: configEntry,
|
||||
manifest: this.manifest,
|
||||
entryUpdated: (entry) =>
|
||||
fireEvent(this, "entry-updated", {
|
||||
entry,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -488,18 +481,7 @@ export class HaIntegrationCard extends LitElement {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
let result: DisableConfigEntryResult;
|
||||
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;
|
||||
}
|
||||
const result = await disableConfigEntry(this.hass, entryId);
|
||||
if (result.require_restart) {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
@@ -515,18 +497,7 @@ export class HaIntegrationCard extends LitElement {
|
||||
private async _enableIntegration(configEntry: ConfigEntry) {
|
||||
const entryId = configEntry.entry_id;
|
||||
|
||||
let result: DisableConfigEntryResult;
|
||||
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;
|
||||
}
|
||||
const result = await enableConfigEntry(this.hass, entryId);
|
||||
|
||||
if (result.require_restart) {
|
||||
showAlertDialog(this, {
|
||||
@@ -590,10 +561,10 @@ export class HaIntegrationCard extends LitElement {
|
||||
if (newName === null) {
|
||||
return;
|
||||
}
|
||||
const result = await updateConfigEntry(this.hass, configEntry.entry_id, {
|
||||
const newEntry = await updateConfigEntry(this.hass, configEntry.entry_id, {
|
||||
title: newName,
|
||||
});
|
||||
fireEvent(this, "entry-updated", { entry: result.config_entry });
|
||||
fireEvent(this, "entry-updated", { entry: newEntry });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -1,9 +1,8 @@
|
||||
import { mdiCloud, mdiPackageVariant, mdiSyncOff } from "@mdi/js";
|
||||
import { mdiCloud, mdiPackageVariant } from "@mdi/js";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { ConfigEntry } from "../../../data/config_entries";
|
||||
import { domainToName, IntegrationManifest } from "../../../data/integration";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
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 configEntry?: ConfigEntry;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
let primary: string;
|
||||
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`
|
||||
|
@@ -9,12 +9,7 @@ import {
|
||||
} from "../../../../../data/zha";
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import {
|
||||
Network,
|
||||
Edge,
|
||||
Node,
|
||||
EdgeOptions,
|
||||
} from "vis-network/peer/esm/vis-network";
|
||||
import { Network, Edge, Node, EdgeOptions } from "vis-network/peer";
|
||||
import "../../../../../common/search/search-input";
|
||||
import "../../../../../components/device/ha-device-picker";
|
||||
import "../../../../../components/ha-button-menu";
|
||||
@@ -26,7 +21,6 @@ import "../../../../../components/ha-checkbox";
|
||||
import type { HaCheckbox } from "../../../../../components/ha-checkbox";
|
||||
import { zhaTabs } from "./zha-config-dashboard";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import "../../../../../components/ha-formfield";
|
||||
|
||||
@customElement("zha-network-visualization-page")
|
||||
export class ZHANetworkVisualizationPage extends LitElement {
|
||||
@@ -34,7 +28,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
|
||||
@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;
|
||||
|
||||
@@ -73,6 +67,8 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
{},
|
||||
{
|
||||
autoResize: true,
|
||||
height: window.innerHeight + "px",
|
||||
width: window.innerWidth + "px",
|
||||
layout: {
|
||||
improvedLayout: true,
|
||||
},
|
||||
@@ -139,35 +135,17 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
"ui.panel.config.zha.visualization.header"
|
||||
)}
|
||||
>
|
||||
${this.narrow
|
||||
? html`
|
||||
<div slot="header">
|
||||
<search-input
|
||||
no-label-float
|
||||
no-underline
|
||||
class="header"
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.filter=${this._filter}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.zha.visualization.highlight_label"
|
||||
)}
|
||||
>
|
||||
</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>`
|
||||
: ""}
|
||||
<div class="table-header">
|
||||
<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
|
||||
.hass=${this.hass}
|
||||
.value=${this.zoomedDeviceId}
|
||||
@@ -177,24 +155,16 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
.deviceFilter=${(device) => this._filterDevices(device)}
|
||||
@value-changed=${this._onZoomToDevice}
|
||||
></ha-device-picker>
|
||||
<div class="controls">
|
||||
<ha-formfield
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.zha.visualization.auto_zoom"
|
||||
)}
|
||||
>
|
||||
<ha-checkbox
|
||||
@change=${this._handleCheckboxChange}
|
||||
.checked=${this._autoZoom}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>
|
||||
<mwc-button @click=${this._refreshTopology}>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.zha.visualization.refresh_topology"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
<ha-checkbox
|
||||
@change=${this._handleCheckboxChange}
|
||||
.checked=${this._autoZoom}
|
||||
></ha-checkbox
|
||||
>${this.hass!.localize("ui.panel.config.zha.visualization.auto_zoom")}
|
||||
<mwc-button @click=${this._refreshTopology}
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.config.zha.visualization.refresh_topology"
|
||||
)}</mwc-button
|
||||
>
|
||||
</div>
|
||||
<div id="visualization"></div>
|
||||
</hass-tabs-subpage>
|
||||
@@ -382,23 +352,30 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
return [
|
||||
css`
|
||||
.header {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
padding: 0 8px;
|
||||
font-family: var(--paper-font-display1_-_font-family);
|
||||
-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;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
height: var(--header-height);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.header > * {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
:host([narrow]) .header {
|
||||
:host([narrow]) .table-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
height: var(--header-height) * 2;
|
||||
height: var(--header-height) * 3;
|
||||
}
|
||||
|
||||
.search-toolbar {
|
||||
@@ -409,34 +386,34 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
}
|
||||
|
||||
search-input {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:host(:not([narrow])) search-input {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
search-input.header {
|
||||
display: block;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
color: var(--secondary-text-color);
|
||||
left: -8px;
|
||||
}
|
||||
|
||||
ha-device-picker {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
top: -4px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
:host(:not([narrow])) ha-device-picker {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
#visualization {
|
||||
height: calc(100% - var(--header-height));
|
||||
width: 100%;
|
||||
mwc-button {
|
||||
font-weight: 500;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
:host([narrow]) #visualization {
|
||||
height: calc(100% - (var(--header-height) * 2));
|
||||
|
||||
:host(:not([narrow])) mwc-button {
|
||||
margin: 5px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -24,7 +24,6 @@ import {
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { fileDownload } from "../../../../../util/file_download";
|
||||
import "../../../ha-config-section";
|
||||
import { showZWaveJSAddNodeDialog } from "./show-dialog-zwave_js-add-node";
|
||||
import { showZWaveJSRemoveNodeDialog } from "./show-dialog-zwave_js-remove-node";
|
||||
@@ -313,7 +312,12 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
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 {
|
||||
|
@@ -9,9 +9,13 @@ import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/map/ha-locations-editor";
|
||||
import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
|
||||
import { getZoneEditorInitData, ZoneMutableParams } from "../../../data/zone";
|
||||
import "../../../components/map/ha-location-editor";
|
||||
import {
|
||||
defaultRadiusColor,
|
||||
getZoneEditorInitData,
|
||||
passiveRadiusColor,
|
||||
ZoneMutableParams,
|
||||
} from "../../../data/zone";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { ZoneDetailDialogParams } from "./show-dialog-zone-detail";
|
||||
@@ -128,19 +132,17 @@ class DialogZoneDetail extends LitElement {
|
||||
)}"
|
||||
.invalid=${iconValid}
|
||||
></paper-input>
|
||||
<ha-locations-editor
|
||||
<ha-location-editor
|
||||
class="flex"
|
||||
.hass=${this.hass}
|
||||
.locations=${this._location(
|
||||
this._latitude,
|
||||
this._longitude,
|
||||
this._radius,
|
||||
this._passive,
|
||||
this._icon
|
||||
)}
|
||||
@location-updated=${this._locationChanged}
|
||||
@radius-updated=${this._radiusChanged}
|
||||
></ha-locations-editor>
|
||||
.location=${this._locationValue(this._latitude, this._longitude)}
|
||||
.radius=${this._radius}
|
||||
.radiusColor=${this._passive
|
||||
? passiveRadiusColor
|
||||
: defaultRadiusColor}
|
||||
.icon=${this._icon}
|
||||
@change=${this._locationChanged}
|
||||
></ha-location-editor>
|
||||
<div class="location">
|
||||
<paper-input
|
||||
.value=${this._latitude}
|
||||
@@ -220,40 +222,11 @@ class DialogZoneDetail extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _location = memoizeOne(
|
||||
(
|
||||
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 _locationValue = memoizeOne((lat, lng) => [Number(lat), Number(lng)]);
|
||||
|
||||
private _locationChanged(ev: CustomEvent) {
|
||||
[this._latitude, this._longitude] = ev.detail.location;
|
||||
}
|
||||
|
||||
private _radiusChanged(ev: CustomEvent) {
|
||||
this._radius = ev.detail.radius;
|
||||
private _locationChanged(ev) {
|
||||
[this._latitude, this._longitude] = ev.currentTarget.location;
|
||||
this._radius = ev.currentTarget.radius;
|
||||
}
|
||||
|
||||
private _passiveChanged(ev) {
|
||||
@@ -319,7 +292,7 @@ class DialogZoneDetail extends LitElement {
|
||||
.location > *:last-child {
|
||||
margin-left: 4px;
|
||||
}
|
||||
ha-locations-editor {
|
||||
ha-location-editor {
|
||||
margin-top: 16px;
|
||||
}
|
||||
a {
|
||||
|
@@ -31,8 +31,11 @@ import { saveCoreConfig } from "../../../data/core";
|
||||
import { subscribeEntityRegistry } from "../../../data/entity_registry";
|
||||
import {
|
||||
createZone,
|
||||
defaultRadiusColor,
|
||||
deleteZone,
|
||||
fetchZones,
|
||||
homeRadiusColor,
|
||||
passiveRadiusColor,
|
||||
updateZone,
|
||||
Zone,
|
||||
ZoneMutableParams,
|
||||
@@ -70,15 +73,6 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _getZones = memoizeOne(
|
||||
(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(
|
||||
(entityState) => ({
|
||||
id: entityState.entity_id,
|
||||
@@ -92,7 +86,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
? homeRadiusColor
|
||||
: entityState.attributes.passive
|
||||
? passiveRadiusColor
|
||||
: zoneRadiusColor,
|
||||
: defaultRadiusColor,
|
||||
location_editable:
|
||||
entityState.entity_id === "zone.home" && this._canEditCore,
|
||||
radius_editable: false,
|
||||
@@ -100,7 +94,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
const storageLocations: MarkerLocation[] = storageItems.map((zone) => ({
|
||||
...zone,
|
||||
radius_color: zone.passive ? passiveRadiusColor : zoneRadiusColor,
|
||||
radius_color: zone.passive ? passiveRadiusColor : defaultRadiusColor,
|
||||
location_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);
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (oldHass && this._stateItems) {
|
||||
@@ -416,9 +410,8 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
if (this.narrow) {
|
||||
return;
|
||||
}
|
||||
this._activeEntry = created.id;
|
||||
await this.updateComplete;
|
||||
await this._map?.updateComplete;
|
||||
this._activeEntry = created.id;
|
||||
this._map?.fitMarker(created.id);
|
||||
}
|
||||
|
||||
@@ -434,9 +427,8 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
if (this.narrow || !fitMap) {
|
||||
return;
|
||||
}
|
||||
this._activeEntry = entry.id;
|
||||
await this.updateComplete;
|
||||
await this._map?.updateComplete;
|
||||
this._activeEntry = entry.id;
|
||||
this._map?.fitMarker(entry.id);
|
||||
}
|
||||
|
||||
|
@@ -11,8 +11,8 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { dump, load } from "js-yaml";
|
||||
import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time";
|
||||
import { isPatternInWord } from "../../../common/string/filter/filter";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import { escapeRegExp } from "../../../common/string/escape_regexp";
|
||||
import { copyToClipboard } from "../../../common/util/copy-clipboard";
|
||||
import "../../../components/entity/ha-entity-picker";
|
||||
import "../../../components/ha-code-editor";
|
||||
@@ -412,68 +412,72 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
}
|
||||
|
||||
computeEntities(hass, _entityFilter, _stateFilter, _attributeFilter) {
|
||||
const entityFilterRegExp =
|
||||
_entityFilter &&
|
||||
RegExp(escapeRegExp(_entityFilter).replace(/\\\*/g, ".*"), "i");
|
||||
const _entityFilterLength = _entityFilter && _entityFilter.length;
|
||||
const _entityFilterLow = _entityFilter && _entityFilter.toLowerCase();
|
||||
|
||||
const stateFilterRegExp =
|
||||
_stateFilter &&
|
||||
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)
|
||||
return Object.keys(hass.states)
|
||||
.map((key) => hass.states[key])
|
||||
.filter((value) => {
|
||||
if (
|
||||
entityFilterRegExp &&
|
||||
!entityFilterRegExp.test(value.entity_id) &&
|
||||
_entityFilter &&
|
||||
!isPatternInWord(
|
||||
_entityFilterLow,
|
||||
0,
|
||||
_entityFilterLength,
|
||||
value.entity_id.toLowerCase(),
|
||||
0,
|
||||
value.entity_id.length,
|
||||
true
|
||||
) &&
|
||||
(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;
|
||||
}
|
||||
|
||||
if (stateFilterRegExp && !stateFilterRegExp.test(value.state)) {
|
||||
if (!value.state.toLowerCase().includes(_stateFilter.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (keyFilterRegExp && valueFilterRegExp) {
|
||||
for (const [key, attributeValue] of Object.entries(
|
||||
value.attributes
|
||||
)) {
|
||||
const match = keyFilterRegExp.test(key);
|
||||
if (match && !multiMode) {
|
||||
if (_attributeFilter !== "") {
|
||||
const attributeFilter = _attributeFilter.toLowerCase();
|
||||
const colonIndex = attributeFilter.indexOf(":");
|
||||
const multiMode = colonIndex !== -1;
|
||||
|
||||
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
|
||||
}
|
||||
if (!match && multiMode) {
|
||||
if (!key.includes(keyFilter) && multiMode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const attributeValue = value.attributes[key];
|
||||
|
||||
if (
|
||||
attributeValue !== undefined &&
|
||||
valueFilterRegExp.test(JSON.stringify(attributeValue))
|
||||
JSON.stringify(attributeValue).toLowerCase().includes(valueFilter)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Layout1d, scroll } from "../../resources/lit-virtualizer";
|
||||
import { Layout1d, scroll } from "@lit-labs/virtualizer";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -100,6 +100,7 @@ class HaLogbook extends LitElement {
|
||||
? scroll({
|
||||
items: this.entries,
|
||||
layout: Layout1d,
|
||||
// @ts-expect-error
|
||||
renderItem: (item: LogbookEntry, index) =>
|
||||
this._renderLogbookItem(item, index),
|
||||
})
|
||||
@@ -353,7 +354,15 @@ class HaLogbook extends LitElement {
|
||||
}
|
||||
|
||||
:host([virtualize]) .container {
|
||||
display: block;
|
||||
position: relative;
|
||||
contain: strict;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.container > * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.narrow .entry {
|
||||
|
@@ -1,22 +1,22 @@
|
||||
import { mdiRefresh } from "@mdi/js";
|
||||
import "@material/mwc-icon-button";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import { css, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import "../../components/entity/ha-entity-picker";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../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 {
|
||||
clearLogbookCache,
|
||||
getLogbookData,
|
||||
LogbookEntry,
|
||||
} from "../../data/logbook";
|
||||
import { fetchPersons } from "../../data/person";
|
||||
import { loadTraceContexts, TraceContexts } from "../../data/trace";
|
||||
import { fetchUsers } from "../../data/user";
|
||||
import "../../layouts/ha-app-layout";
|
||||
@@ -44,7 +44,7 @@ export class HaPanelLogbook extends LitElement {
|
||||
|
||||
@state() private _ranges?: DateRangePickerRanges;
|
||||
|
||||
private _fetchUserPromise?: Promise<void>;
|
||||
private _fetchUserDone?: Promise<unknown>;
|
||||
|
||||
@state() private _userIdToName = {};
|
||||
|
||||
@@ -136,7 +136,7 @@ export class HaPanelLogbook extends LitElement {
|
||||
super.firstUpdated(changedProps);
|
||||
this.hass.loadBackendTranslation("title");
|
||||
|
||||
this._fetchUserPromise = this._fetchUserNames();
|
||||
this._fetchUserDone = this._fetchUserNames();
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
@@ -198,19 +198,23 @@ export class HaPanelLogbook extends LitElement {
|
||||
private async _fetchUserNames() {
|
||||
const userIdToName = {};
|
||||
|
||||
// Start loading users
|
||||
const userProm = this.hass.user?.is_admin && fetchUsers(this.hass);
|
||||
// Start loading all the data
|
||||
const personProm = fetchPersons(this.hass);
|
||||
const userProm = this.hass.user!.is_admin && fetchUsers(this.hass);
|
||||
|
||||
// Process persons
|
||||
Object.values(this.hass.states).forEach((entity) => {
|
||||
if (
|
||||
entity.attributes.user_id &&
|
||||
computeStateDomain(entity) === "person"
|
||||
) {
|
||||
this._userIdToName[entity.attributes.user_id] =
|
||||
entity.attributes.friendly_name;
|
||||
const persons = await personProm;
|
||||
|
||||
for (const person of persons.storage) {
|
||||
if (person.user_id) {
|
||||
userIdToName[person.user_id] = person.name;
|
||||
}
|
||||
});
|
||||
}
|
||||
for (const person of persons.config) {
|
||||
if (person.user_id) {
|
||||
userIdToName[person.user_id] = person.name;
|
||||
}
|
||||
}
|
||||
|
||||
// Process users
|
||||
if (userProm) {
|
||||
@@ -258,7 +262,7 @@ export class HaPanelLogbook extends LitElement {
|
||||
this._entityId
|
||||
),
|
||||
isComponentLoaded(this.hass, "trace") ? loadTraceContexts(this.hass) : {},
|
||||
this._fetchUserPromise,
|
||||
this._fetchUserDone,
|
||||
]);
|
||||
|
||||
this._entries = entries;
|
||||
|
@@ -9,12 +9,11 @@ import {
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
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 { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { throttle } from "../../../common/util/throttle";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import { fetchUsers } from "../../../data/user";
|
||||
import { getLogbookData, LogbookEntry } from "../../../data/logbook";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
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 _logbookEntries?: LogbookEntry[];
|
||||
|
||||
@state() private _persons = {};
|
||||
|
||||
@state() private _configEntities?: EntityConfig[];
|
||||
|
||||
@state() private _userIdToName = {};
|
||||
|
||||
private _lastLogbookDate?: Date;
|
||||
|
||||
private _fetchUserPromise?: Promise<void>;
|
||||
|
||||
private _throttleGetLogbookEntries = throttle(() => {
|
||||
this._getLogBookData();
|
||||
}, 10000);
|
||||
@@ -117,7 +114,7 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._fetchUserPromise = this._fetchUserNames();
|
||||
this._fetchPersonNames();
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
@@ -202,7 +199,7 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
|
||||
virtualize
|
||||
.hass=${this.hass}
|
||||
.entries=${this._logbookEntries}
|
||||
.userIdToName=${this._userIdToName}
|
||||
.userIdToName=${this._persons}
|
||||
></ha-logbook>
|
||||
`
|
||||
: html`
|
||||
@@ -232,16 +229,13 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
|
||||
const lastDate = this._lastLogbookDate || hoursToShowDate;
|
||||
const now = new Date();
|
||||
|
||||
const [newEntries] = await Promise.all([
|
||||
getLogbookData(
|
||||
this.hass,
|
||||
lastDate.toISOString(),
|
||||
now.toISOString(),
|
||||
this._configEntities!.map((entity) => entity.entity).toString(),
|
||||
true
|
||||
),
|
||||
this._fetchUserPromise,
|
||||
]);
|
||||
const newEntries = await getLogbookData(
|
||||
this.hass,
|
||||
lastDate.toISOString(),
|
||||
now.toISOString(),
|
||||
this._configEntities!.map((entity) => entity.entity).toString(),
|
||||
true
|
||||
);
|
||||
|
||||
const logbookEntries = this._logbookEntries
|
||||
? [...newEntries, ...this._logbookEntries]
|
||||
@@ -254,34 +248,20 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
|
||||
this._lastLogbookDate = now;
|
||||
}
|
||||
|
||||
private async _fetchUserNames() {
|
||||
const userIdToName = {};
|
||||
private _fetchPersonNames() {
|
||||
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) => {
|
||||
if (
|
||||
entity.attributes.user_id &&
|
||||
computeStateDomain(entity) === "person"
|
||||
) {
|
||||
this._userIdToName[entity.attributes.user_id] =
|
||||
this._persons[entity.attributes.user_id] =
|
||||
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 {
|
||||
|
@@ -1,5 +1,14 @@
|
||||
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
||||
import { LatLngTuple } from "leaflet";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
Circle,
|
||||
CircleMarker,
|
||||
LatLngTuple,
|
||||
Layer,
|
||||
Map,
|
||||
Marker,
|
||||
Polyline,
|
||||
TileLayer,
|
||||
} from "leaflet";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -8,106 +17,32 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} 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 { 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 "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import { fetchRecent } from "../../../data/history";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "../../../components/map/ha-entity-marker";
|
||||
import "../../map/ha-entity-marker";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { installResizeObserver } from "../common/install-resize-observer";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
import { EntityConfig } from "../entity-rows/types";
|
||||
import { LovelaceCard } 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")
|
||||
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() {
|
||||
await import("../editor/config-elements/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 };
|
||||
}
|
||||
|
||||
@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 {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
@@ -138,29 +196,22 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
return html`
|
||||
<ha-card id="card" .header=${this._config.title}>
|
||||
<div id="root">
|
||||
<ha-map
|
||||
.hass=${this.hass}
|
||||
.entities=${this._getEntities(
|
||||
this.hass.states,
|
||||
this._config,
|
||||
this._configEntities
|
||||
)}
|
||||
.paths=${this._getHistoryPaths(this._config, this._history)}
|
||||
.darkMode=${this._config.dark_mode}
|
||||
></ha-map>
|
||||
<mwc-icon-button
|
||||
<div
|
||||
id="map"
|
||||
class=${classMap({ dark: this._config.dark_mode === true })}
|
||||
></div>
|
||||
<ha-icon-button
|
||||
@click=${this._fitMap}
|
||||
tabindex="0"
|
||||
icon="hass:image-filter-center-focus"
|
||||
title="Reset focus"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiImageFilterCenterFocus}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues) {
|
||||
protected shouldUpdate(changedProps) {
|
||||
if (!changedProps.has("hass") || changedProps.size > 1) {
|
||||
return true;
|
||||
}
|
||||
@@ -177,7 +228,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
|
||||
// Check if any state has changed
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -187,12 +238,17 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
if (this.isConnected) {
|
||||
this.loadMap();
|
||||
}
|
||||
const root = this.shadowRoot!.getElementById("root");
|
||||
|
||||
if (!this._config || this.isPanel || !root) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._attachObserver();
|
||||
|
||||
if (!this._config.aspect_ratio) {
|
||||
root.style.paddingBottom = "100%";
|
||||
return;
|
||||
@@ -207,86 +263,172 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
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) {
|
||||
const minute = 60000;
|
||||
if (changedProps.has("_config")) {
|
||||
this._getHistory();
|
||||
} else if (Date.now() - this._date!.getTime() >= MINUTE) {
|
||||
} else if (Date.now() - this._date!.getTime() >= minute) {
|
||||
this._getHistory();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _fitMap() {
|
||||
this._map?.fitMap();
|
||||
private get _mapEl(): HTMLDivElement {
|
||||
return this.shadowRoot!.getElementById("map") as HTMLDivElement;
|
||||
}
|
||||
|
||||
private _getColor(entityId: string): string {
|
||||
let color = this._colorDict[entityId];
|
||||
if (color) {
|
||||
return color;
|
||||
private async loadMap(): Promise<void> {
|
||||
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
|
||||
this._mapEl,
|
||||
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;
|
||||
}
|
||||
|
||||
private _getEntities = memoizeOne(
|
||||
(
|
||||
states: HassEntities,
|
||||
config: MapCardConfig,
|
||||
configEntities?: string[]
|
||||
) => {
|
||||
if (!states || !config) {
|
||||
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 _drawEntities(): void {
|
||||
const hass = this.hass;
|
||||
const map = this._leafletMap;
|
||||
const config = this._config;
|
||||
const Leaflet = this.Leaflet;
|
||||
if (!hass || !map || !config || !Leaflet) {
|
||||
return;
|
||||
}
|
||||
);
|
||||
|
||||
private _getHistoryPaths = memoizeOne(
|
||||
(
|
||||
config: MapCardConfig,
|
||||
history?: HassEntity[][]
|
||||
): HaMapPaths[] | undefined => {
|
||||
if (!config.hours_to_show || !history) {
|
||||
return undefined;
|
||||
if (this._mapItems) {
|
||||
this._mapItems.forEach((marker) => marker.remove());
|
||||
}
|
||||
const mapItems: Layer[] = (this._mapItems = []);
|
||||
|
||||
if (this._mapZones) {
|
||||
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[] = [];
|
||||
|
||||
for (const entityStates of history) {
|
||||
// DRAW history
|
||||
if (this._config!.hours_to_show && this._history) {
|
||||
for (const entityStates of this._history) {
|
||||
if (entityStates?.length <= 1) {
|
||||
continue;
|
||||
}
|
||||
const entityId = entityStates[0].entity_id;
|
||||
|
||||
// filter location data from states and remove all invalid locations
|
||||
const points = entityStates.reduce(
|
||||
(accumulator: LatLngTuple[], entityState) => {
|
||||
const latitude = entityState.attributes.latitude;
|
||||
const longitude = entityState.attributes.longitude;
|
||||
const path = entityStates.reduce(
|
||||
(accumulator: LatLngTuple[], state) => {
|
||||
const latitude = state.attributes.latitude;
|
||||
const longitude = state.attributes.longitude;
|
||||
if (latitude && longitude) {
|
||||
accumulator.push([latitude, longitude] as LatLngTuple);
|
||||
}
|
||||
@@ -295,15 +437,162 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
[]
|
||||
) as LatLngTuple[];
|
||||
|
||||
paths.push({
|
||||
points,
|
||||
color: this._getColor(entityStates[0].entity_id),
|
||||
gradualOpacity: 0.8,
|
||||
});
|
||||
// DRAW HISTORY
|
||||
for (
|
||||
let markerIndex = 0;
|
||||
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> {
|
||||
this._date = new Date();
|
||||
@@ -312,7 +601,9 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
return;
|
||||
}
|
||||
|
||||
const entityIds = this._configEntities!.join(",");
|
||||
const entityIds = this._configEntities!.map((entity) => entity.entity).join(
|
||||
","
|
||||
);
|
||||
const endTime = new Date();
|
||||
const startTime = new Date();
|
||||
startTime.setHours(endTime.getHours() - this._config!.hours_to_show!);
|
||||
@@ -333,6 +624,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
if (stateHistory.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._history = stateHistory;
|
||||
}
|
||||
|
||||
@@ -344,10 +636,13 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
this._history = undefined;
|
||||
} else {
|
||||
// remove unused entities
|
||||
const configEntityIds = this._configEntities?.map(
|
||||
(configEntity) => configEntity.entity
|
||||
);
|
||||
this._history = this._history!.reduce(
|
||||
(accumulator: HassEntity[][], entityStates) => {
|
||||
const entityId = entityStates[0].entity_id;
|
||||
if (this._configEntities?.includes(entityId)) {
|
||||
if (configEntityIds?.includes(entityId)) {
|
||||
accumulator.push(entityStates);
|
||||
}
|
||||
return accumulator;
|
||||
@@ -365,7 +660,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
ha-map {
|
||||
#map {
|
||||
z-index: 0;
|
||||
border: none;
|
||||
position: absolute;
|
||||
@@ -376,7 +671,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
mwc-icon-button {
|
||||
ha-icon-button {
|
||||
position: absolute;
|
||||
top: 75px;
|
||||
left: 3px;
|
||||
@@ -390,6 +685,14 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
:host([ispanel]) #root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.light {
|
||||
color: #000000;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -33,18 +33,18 @@ export class HuiActionEditor extends LitElement {
|
||||
@property() protected hass?: HomeAssistant;
|
||||
|
||||
get _navigation_path(): string {
|
||||
const config = this.config as NavigateActionConfig | undefined;
|
||||
return config?.navigation_path || "";
|
||||
const config = this.config as NavigateActionConfig;
|
||||
return config.navigation_path || "";
|
||||
}
|
||||
|
||||
get _url_path(): string {
|
||||
const config = this.config as UrlActionConfig | undefined;
|
||||
return config?.url_path || "";
|
||||
const config = this.config as UrlActionConfig;
|
||||
return config.url_path || "";
|
||||
}
|
||||
|
||||
get _service(): string {
|
||||
const config = this.config as CallServiceActionConfig;
|
||||
return config?.service || "";
|
||||
return config.service || "";
|
||||
}
|
||||
|
||||
private _serviceAction = memoizeOne(
|
||||
|
@@ -29,7 +29,6 @@ export class HuiInputListEditor extends LitElement {
|
||||
.index=${index}
|
||||
@value-changed=${this._valueChanged}
|
||||
@blur=${this._consolidateEntries}
|
||||
@keydown=${this._handleKeyDown}
|
||||
><ha-icon-button
|
||||
slot="suffix"
|
||||
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 {
|
||||
const target = ev.target! as EditorTarget;
|
||||
if (target.value === "") {
|
||||
|
@@ -15,8 +15,7 @@ import "../../components/hui-action-editor";
|
||||
import "../../components/hui-entity-editor";
|
||||
import "../../components/hui-theme-select-editor";
|
||||
import { LovelaceCardEditor } from "../../types";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
import { EditorTarget } from "../types";
|
||||
import { actionConfigStruct, EditorTarget } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
|
||||
const cardConfigStruct = object({
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user