Compare commits

...

104 Commits

Author SHA1 Message Date
Joakim Sørensen
0bfeb22209 Move snapshot toggle to persistent checkbox 2021-06-15 16:40:24 +00:00
Philip Allgaier
0e3eed0563 Fix supervisor text "error_addon_not_started" (#9412) 2021-06-15 15:38:14 +02:00
Joakim Sørensen
1b1676cecc Use poll for webpack for WSL (#9425) 2021-06-15 15:34:15 +02:00
GitHub Action
d911fe6a0b Translation update 2021-06-15 00:48:36 +00:00
Bram Kragten
22253a3385 Add header and description to progress options flow (#9423) 2021-06-15 01:10:24 +02:00
GitHub Action
38640c99e3 Translation update 2021-06-14 00:48:26 +00:00
GitHub Action
d6df8bddea Translation update 2021-06-13 00:48:28 +00:00
GitHub Action
ddfc4bd98e Translation update 2021-06-12 00:48:12 +00:00
Shane Qi
3d6674325c Fix the issue that logbook card doesn't translate context.user_id to name if it's a user's id. (#9383)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-06-10 14:51:24 +02:00
Bram Kragten
194829f5b1 Generalize map (#9331)
* Generalize map

* Fix path opacity

* Add fitZones
2021-06-10 14:22:44 +02:00
GitHub Action
11a77253f4 Translation update 2021-06-10 00:49:02 +00:00
GitHub Action
67be2343f8 Translation update 2021-06-09 00:48:19 +00:00
Joakim Sørensen
e9b1b3d853 Fix issues with restoring snapshot during onboarding (#9385)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-06-08 17:57:53 +02:00
Bram Kragten
8a33d174d7 FIx selecting service/url path action after choosing default action (#9376)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-06-08 14:24:58 +02:00
Joakim Sørensen
226d6216b7 Build wheels for 3.9-alpine3.13 (#9390) 2021-06-08 13:07:02 +02:00
GitHub Action
1925bb01be Translation update 2021-06-08 00:49:18 +00:00
Bram Kragten
82a4806e01 Change line logic 2021-06-07 10:45:57 +02:00
Joakim Sørensen
ce419fae7b Add password confirmation to snapshot creation (#9349)
* Add password confirmation to snapshot creation

* Remove confirm_password before sending

* change layout

* style changes

* Adjust styling

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-06-07 10:45:20 +02:00
Joakim Sørensen
c68b76e2da Add hardware dialog (#9348)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-06-07 10:16:33 +02:00
Joakim Sørensen
342020b420 Fix downloads on mobile (#9375) 2021-06-07 10:15:43 +02:00
GitHub Action
1e6e99e3c7 Translation update 2021-06-07 00:49:13 +00:00
GitHub Action
2e9aafc377 Translation update 2021-06-06 00:49:09 +00:00
dependabot[bot]
299c863f49 Bump ws from 6.2.1 to 6.2.2 (#9372) 2021-06-05 23:52:13 +02:00
Bram Kragten
c2792a28ba Move attributes down in more info person and timer (#9368) 2021-06-05 12:52:27 +02:00
GitHub Action
635a027a8e Translation update 2021-06-05 02:40:15 +00:00
Will Adler
a45b8ca8e7 Add period to end of sentence (#9361) 2021-06-04 08:57:03 +02:00
GitHub Action
1e6e945a07 Translation update 2021-06-04 02:52:56 +00:00
Bram Kragten
f71157c24d Remove tsc from pre commit (#9359) 2021-06-03 22:57:03 +02:00
Bram Kragten
e87a2b36cf Bumped version to 20210603.0 2021-06-03 22:51:53 +02:00
Bram Kragten
5418474f64 Polyfill globalThis in latest build (#9352) 2021-06-03 22:50:33 +02:00
Philip Allgaier
8836ba6ceb Pick the correct backend-selected active theme (#9357) 2021-06-03 22:48:52 +02:00
Bram Kragten
509c5b497a Disable babel compact option (#9335) 2021-06-03 12:34:30 -07:00
Joakim Sørensen
e00bcc9f48 Better exit navigation for my-ingress (#9342) 2021-06-03 10:01:12 -07:00
Joakim Sørensen
bdef9fd040 Add missing media folder to snapshot (#9341) 2021-06-03 10:21:04 +02:00
GitHub Action
c956491ec5 Translation update 2021-06-03 03:48:04 +00:00
Bram Kragten
68bc549d6a Use HLS light build (#9338)
* Use HLS light build

* Bump hls, backBufferLength
2021-06-02 18:34:18 +02:00
Bram Kragten
9c64eafc21 Fix ZHA visualization (#9337) 2021-06-02 18:33:55 +02:00
Bram Kragten
b05e86d442 Fix noUnderline in search input (#9339) 2021-06-02 18:33:44 +02:00
Bram Kragten
fe5f9576c6 Fix dev 2021-06-02 10:11:29 +02:00
Brynley McDonald
1b282b65b7 Add QR code to long lived access tokens dialog (#8948)
* Add QR code to long lived access tokens dialog

* Apply suggestions from code review

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Further changes from code review

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-06-02 09:59:22 +02:00
GitHub Action
e49664bad3 Translation update 2021-06-02 04:10:37 +00:00
Bram Kragten
2a30b55a43 Bumped version to 20210601.1 2021-06-01 21:30:51 +02:00
Paulus Schoutsen
9d0b20adce Add support for system options v2 (#9332)
* Add support for system options v2

* Update src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Update dialog-config-entry-system-options.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-06-01 12:22:25 -07:00
Bram Kragten
acd5e1c081 Fix back button too wide on mobile (#9333) 2021-06-01 12:12:00 -07:00
Bram Kragten
cc1c5e45b2 Display error when enabling/disabling config entries (#9325) 2021-06-01 21:03:00 +02:00
Bram Kragten
038199c447 Change the type of debounce, use arrow functions (#9328) 2021-06-01 11:53:45 -07:00
Bram Kragten
8a1eab7ceb Cleanup virtualizer styles (#9327) 2021-06-01 11:51:30 -07:00
Joakim Sørensen
bc5bd35448 Filter adapters with information from the Supervisor (#9322)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-06-01 20:12:54 +02:00
Bram Kragten
1795fd56b7 Don't rotate chart axis labels (#9329) 2021-06-01 10:28:03 -07:00
Bram Kragten
4a7c33edad Bumped version to 20210601.0 2021-06-01 11:40:37 +02:00
rianadon
797f60d725 Show pressure units in weather details card (#9295) 2021-06-01 11:40:04 +02:00
Bram Kragten
2427d68aa1 Use local version 0.7 of lit-virtualizer (#9321) 2021-06-01 11:39:15 +02:00
GitHub Action
00c6b0f8ed Translation update 2021-06-01 04:14:44 +00:00
Paulus Schoutsen
7b8d4ab3d6 Update translations 2021-05-31 15:50:47 -07:00
Paulus Schoutsen
07a1a805f6 Bumped version to 20210531.1 2021-05-31 15:44:59 -07:00
Paulus Schoutsen
d8bab6aba9 Add support for disable polling system option (#9316) 2021-05-31 15:40:50 -07:00
Bram Kragten
a930e2dc75 Fix store auth disappearing (#9312) 2021-05-31 15:35:51 -07:00
J. Nick Koston
2eb35668fa Seperate addresses in network configuration (#9319) 2021-05-31 15:31:33 -07:00
GitHub Action
07f4e5ac5c Translation update 2021-05-31 03:47:12 +00:00
Paulus Schoutsen
db82a90414 Bumped version to 20210531.0 2021-05-30 20:17:09 -07:00
Bram Kragten
51a693badf Convert ha-store-auth-card to Lit/TS/ha-card (#9300) 2021-05-30 20:16:45 -07:00
Bram Kragten
2aa8f5b352 Dev states: replace pattern in word by wildcard search (#9288) 2021-05-30 20:11:53 -07:00
Bram Kragten
93b3b8f985 Fix editor structs (#9286) 2021-05-30 20:08:46 -07:00
Bram Kragten
92c8bd80b5 Catch translation errors (#9299) 2021-05-30 17:02:03 +02:00
Philip Allgaier
528af0157d Move entity attribution out of attribute expansion panel (#9296) 2021-05-30 16:06:22 +02:00
Bram Kragten
10a77b6278 Update translations 2021-05-30 16:02:03 +02:00
GitHub Action
03bbf6a582 Translation update 2021-05-30 03:38:49 +00:00
GitHub Action
63fcb649d2 Translation update 2021-05-29 03:21:09 +00:00
Bram Kragten
4f60a92b92 Fix default themes (#9290)
* Fix default themes

* Simplify pick theme row
2021-05-28 20:02:28 -07:00
Bram Kragten
0419c1a41f Fix icon loading (#9289) 2021-05-28 19:54:28 -07:00
Joakim Sørensen
2d5ae78521 Add selection to snapshot table for mass deletion (#9284)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-05-28 15:49:40 +02:00
Joakim Sørensen
959134df02 Better secrets support in add-on configuration (#9275) 2021-05-28 14:37:16 +02:00
Bram Kragten
a9f9fc4ce2 Bumped version to 20210528.0 2021-05-28 12:26:38 +02:00
dependabot[bot]
cfb370a3c8 Bump dns-packet from 1.3.1 to 1.3.4 (#9281)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-28 12:25:46 +02:00
Bram Kragten
353435c8d5 Fix icon db loading (#9280) 2021-05-28 11:54:20 +02:00
Bram Kragten
c8c85d096b Show less ticks in charts (#9279) 2021-05-28 10:16:37 +02:00
GitHub Action
19c9c8f227 Translation update 2021-05-28 03:03:41 +00:00
Bram Kragten
6ea2a29eea Hide attribute measurement and editable attributes (#9272) 2021-05-27 11:48:27 +02:00
Joakim Sørensen
59f3f819a6 Revert name change from selectedTheme to selectedThemeSettings (#9273) 2021-05-27 10:21:58 +02:00
GitHub Action
93e8f52880 Translation update 2021-05-27 02:45:51 +00:00
Joakim Sørensen
02810efcc4 Replace Hass_io_ prefix for snapshot downloads (#9270) 2021-05-26 21:56:27 +02:00
Bram Kragten
4b9be7ce16 Fix entity filtering in dev states (#9268) 2021-05-26 17:27:45 +02:00
Bram Kragten
f3ec09e480 Bumped version to 20210526.0 2021-05-26 16:58:31 +02:00
Bram Kragten
8291a84e3e Hide network config when not loaded (#9265) 2021-05-26 07:53:54 -07:00
J. Nick Koston
b0e1f0f73a Add network configuration (#9210)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-05-26 16:44:15 +02:00
Bram Kragten
a66b966e7d Fix a bunch of updates triggering updated (#9260) 2021-05-26 16:29:50 +02:00
Philip Allgaier
5f56040c64 Add friendly_name to dev tools "Entity" column + fuzzy search (#7582) 2021-05-26 15:29:51 +02:00
Bram Kragten
eaccd22267 Fix chartjs deprecation warnings (#9261) 2021-05-26 15:05:01 +02:00
Bram Kragten
27845a7345 Fix logbook height (#9258) 2021-05-26 12:44:10 +02:00
Bram Kragten
f7ef8180e4 Guard for undefined item in quick bar (#9259) 2021-05-26 12:43:59 +02:00
Bram Kragten
5958eb9a55 Minor dependency bumps (#9249) 2021-05-26 12:04:39 +02:00
Bram Kragten
3ef2912b60 Fix typo in translation key 2021-05-26 11:19:09 +02:00
Joakim Sørensen
fa9c6a765a Replace closing with closed in dialogs (#9257) 2021-05-26 11:10:27 +02:00
Bram Kragten
c4a8899780 Bump idb-keyval (#9248)
https://github.com/jakearchibald/idb-keyval#updating-from-3x
2021-05-26 10:22:38 +02:00
Bram Kragten
3cc4628d03 Bump test dependencies (#9244) 2021-05-26 10:02:02 +02:00
GitHub Action
b6c5223221 Translation update 2021-05-26 02:25:19 +00:00
Philip Allgaier
cbd6d4251c Prevent shrinking of percent value in supervisor metrics (#9033) 2021-05-26 00:24:30 +02:00
Bram Kragten
fdcbb5b432 Bump js-yaml (#9245) 2021-05-26 00:13:58 +02:00
Bram Kragten
de09e31815 Fix resetting theme, only fallback to light when theme doesnt support… (#9253) 2021-05-26 00:11:17 +02:00
Philip Allgaier
f55e911313 Prevent formatting for unknown attribute (#9252) 2021-05-26 00:08:41 +02:00
Bram Kragten
465a91dbf3 Fix circulair progress producing scrollbars (#9247) 2021-05-25 23:59:24 +02:00
Bram Kragten
835a7833ae Bump memoize one (#9243) 2021-05-25 23:53:58 +02:00
Bram Kragten
179717d40c Fix rollup build (#9246) 2021-05-25 23:51:31 +02:00
Philip Allgaier
3d4d789f7f Detect and format date & timestamp attributes (#9074) 2021-05-25 22:39:21 +02:00
297 changed files with 10758 additions and 4022 deletions

View File

@@ -28,9 +28,7 @@
"__BUILD__": false,
"__VERSION__": false,
"__STATIC_PATH__": false,
"Polymer": true,
"webkitSpeechRecognition": false,
"ResizeObserver": false
"Polymer": true
},
"env": {
"browser": true,
@@ -106,5 +104,6 @@
"lit/attribute-value-entities": 0
},
"plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"],
"processor": "disable/disable"
"processor": "disable/disable",
"ignorePatterns": ["src/resources/lit-virtualizer/*"]
}

View File

@@ -6,7 +6,6 @@ on:
- published
env:
WHEELS_TAG: 3.8-alpine3.12
PYTHON_VERSION: 3.8
NODE_VERSION: 12.1
@@ -64,6 +63,9 @@ 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
@@ -73,7 +75,7 @@ jobs:
- name: Build wheels
uses: home-assistant/wheels@master
with:
tag: ${{ env.WHEELS_TAG }}
tag: ${{ matrix.tag }}
arch: ${{ matrix.arch }}
wheels-host: ${{ secrets.WHEELS_HOST }}
wheels-key: ${{ secrets.WHEELS_KEY }}

21
.gitignore vendored
View File

@@ -1,10 +1,17 @@
.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]
@@ -14,11 +21,8 @@ hass_frontend/*
# venv stuff
pyvenv.cfg
pip-selfcheck.json
venv
venv/*
.venv
lib
bin
dist
# vscode
.vscode/*
@@ -31,9 +35,8 @@ src/cast/dev_const.ts
# Secrets
.lokalise_token
yarn-error.log
#asdf
# asdf
.tool-versions
# Home Assistant config

4
.mocharc.cjs Normal file
View File

@@ -0,0 +1,4 @@
module.exports = {
require: "test-mocha/testconf.js",
timeout: 10000,
};

View File

@@ -52,6 +52,7 @@ module.exports.terserOptions = (latestBuild) => ({
module.exports.babelOptions = ({ latestBuild }) => ({
babelrc: false,
compact: false,
presets: [
!latestBuild && [
"@babel/preset-env",
@@ -79,12 +80,6 @@ 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");

View File

@@ -1,4 +1,5 @@
// Tasks to run webpack.
const fs = require("fs");
const gulp = require("gulp");
const webpack = require("webpack");
const WebpackDevServer = require("webpack-dev-server");
@@ -18,6 +19,11 @@ const bothBuilds = (createConfigFunc, params) => [
createConfigFunc({ ...params, latestBuild: false }),
];
const isWsl = fs
.readFileSync("/proc/version", "utf-8")
.toLocaleLowerCase()
.includes("microsoft");
/**
* @param {{
* compiler: import("webpack").Compiler,
@@ -79,7 +85,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/ },
{ ignored: /build-translations/, poll: isWsl },
doneHandler()
);
gulp.watch(
@@ -137,7 +143,7 @@ gulp.task("webpack-watch-hassio", () => {
isProdBuild: false,
latestBuild: true,
})
).watch({ ignored: /build-translations/ }, doneHandler());
).watch({ ignored: /build-translations/, poll: isWsl }, doneHandler());
gulp.watch(
path.join(paths.translations_src, "en.json"),

View File

@@ -52,16 +52,11 @@ const createRollupConfig = ({
browser: true,
rootDir: paths.polymer_dir,
}),
commonjs({
namedExports: {
"js-yaml": ["safeDump", "safeLoad"],
},
}),
commonjs(),
json(),
babel({
...bundle.babelOptions({ latestBuild }),
extensions,
exclude: bundle.babelExclude(),
babelHelpers: isWDS ? "inline" : "bundled",
}),
string({

View File

@@ -47,7 +47,6 @@ const createWebpackConfig = ({
rules: [
{
test: /\.m?js$|\.ts$/,
exclude: bundle.babelExclude(),
use: {
loader: "babel-loader",
options: bundle.babelOptions({ latestBuild }),
@@ -116,8 +115,9 @@ 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(
require.resolve(
"@lit-labs/virtualizer/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js"
path.resolve(
paths.polymer_dir,
"src/resources/lit-virtualizer/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js"
)
),
path.resolve(paths.polymer_dir, "src/resources/EventTarget-ponyfill.js")

View File

@@ -1,7 +1,7 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { safeLoad } from "js-yaml";
import { load } from "js-yaml";
import { createCardElement } from "../../../src/panels/lovelace/create-element/create-card-element";
class DemoCard extends PolymerElement {
@@ -80,7 +80,7 @@ class DemoCard extends PolymerElement {
card.removeChild(card.lastChild);
}
const el = this._createCardElement(safeLoad(config.config)[0]);
const el = this._createCardElement(load(config.config)[0]);
card.appendChild(el);
this._getSize(el);
}

View File

@@ -1,4 +1,4 @@
import { safeDump } from "js-yaml";
import { dump } from "js-yaml";
import { html, css, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../src/components/ha-card";
@@ -56,7 +56,7 @@ export class DemoAutomationDescribeAction extends LitElement {
(conf) => html`
<div class="action">
<span>${describeAction(this.hass, conf as any)}</span>
<pre>${safeDump(conf)}</pre>
<pre>${dump(conf)}</pre>
</div>
`
)}

View File

@@ -1,4 +1,4 @@
import { safeDump } from "js-yaml";
import { dump } from "js-yaml";
import { html, css, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import "../../../src/components/ha-card";
@@ -26,7 +26,7 @@ export class DemoAutomationDescribeCondition extends LitElement {
(conf) => html`
<div class="condition">
<span>${describeCondition(conf as any)}</span>
<pre>${safeDump(conf)}</pre>
<pre>${dump(conf)}</pre>
</div>
`
)}

View File

@@ -1,4 +1,4 @@
import { safeDump } from "js-yaml";
import { dump } from "js-yaml";
import { html, css, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import "../../../src/components/ha-card";
@@ -29,7 +29,7 @@ export class DemoAutomationDescribeTrigger extends LitElement {
(conf) => html`
<div class="trigger">
<span>${describeTrigger(conf as any)}</span>
<pre>${safeDump(conf)}</pre>
<pre>${dump(conf)}</pre>
</div>
`
)}

View File

@@ -28,10 +28,11 @@ 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,
});
@@ -64,6 +65,9 @@ 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",
});
@@ -136,6 +140,7 @@ const configEntries: Array<{
{ items: [loadedEntry] },
{ items: [configPanelEntry] },
{ items: [optionsFlowEntry] },
{ items: [disabledPollingEntry] },
{ items: [nameAsDomainEntry] },
{ items: [longNameEntry] },
{ items: [longNonBreakingNameEntry] },

View File

@@ -3,6 +3,7 @@ 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,
@@ -11,7 +12,7 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state, query } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
@@ -27,6 +28,7 @@ import {
HassioAddonDetails,
HassioAddonSetOptionParams,
setHassioAddonOption,
validateHassioAddonOption,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
@@ -38,6 +40,13 @@ 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;
@@ -125,6 +134,7 @@ 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 ||
@@ -269,6 +279,14 @@ 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,
});

View File

@@ -977,6 +977,7 @@ 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}`,

View File

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

View File

@@ -64,6 +64,7 @@ class SupervisorMetric extends LitElement {
.value {
width: 48px;
padding-right: 4px;
flex-shrink: 0;
}
`;
}

View File

@@ -5,6 +5,7 @@ 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";
@@ -44,6 +45,9 @@ 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 });
}
@@ -64,6 +68,8 @@ 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;
@@ -78,10 +84,14 @@ 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) {
@@ -101,8 +111,12 @@ 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.supervisor) {
if (!this.onboarding && !this.supervisor) {
return html``;
}
const foldersSection =
@@ -114,14 +128,16 @@ export class SupervisorSnapshotContent extends LitElement {
${this.snapshot
? html`<div class="details">
${this.snapshot.type === "full"
? this.supervisor.localize("snapshot.full_snapshot")
: this.supervisor.localize("snapshot.partial_snapshot")}
? this._localize("full_snapshot")
: this._localize("partial_snapshot")}
(${Math.ceil(this.snapshot.size * 10) / 10 + " MB"})<br />
${formatDateTime(new Date(this.snapshot.date), this.hass.locale)}
${this.hass
? formatDateTime(new Date(this.snapshot.date), this.hass.locale)
: this.snapshot.date}
</div>`
: html`<paper-input
name="snapshotName"
.label=${this.supervisor.localize("snapshot.name")}
.label=${this.supervisor?.localize("snapshot.name") || "Name"}
.value=${this.snapshotName}
@value-changed=${this._handleTextValueChanged}
>
@@ -129,13 +145,11 @@ export class SupervisorSnapshotContent extends LitElement {
${!this.snapshot || this.snapshot.type === "full"
? html`<div class="sub-header">
${!this.snapshot
? this.supervisor.localize("snapshot.type")
: this.supervisor.localize("snapshot.select_type")}
? this._localize("type")
: this._localize("select_type")}
</div>
<div class="snapshot-types">
<ha-formfield
.label=${this.supervisor.localize("snapshot.full_snapshot")}
>
<ha-formfield .label=${this._localize("full_snapshot")}>
<ha-radio
@change=${this._handleRadioValueChanged}
value="full"
@@ -144,9 +158,7 @@ export class SupervisorSnapshotContent extends LitElement {
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.supervisor!.localize("snapshot.partial_snapshot")}
>
<ha-formfield .label=${this._localize("partial_snapshot")}>
<ha-radio
@change=${this._handleRadioValueChanged}
value="partial"
@@ -157,9 +169,9 @@ export class SupervisorSnapshotContent extends LitElement {
</ha-formfield>
</div>`
: ""}
${this.snapshot && this.snapshotType === "partial"
? html`
${this.snapshot.homeassistant
${this.snapshotType === "partial"
? html`<div class="partial-picker">
${this.snapshot && this.snapshot.homeassistant
? html`
<ha-formfield
.label=${html`<supervisor-formfield-label
@@ -179,15 +191,11 @@ export class SupervisorSnapshotContent extends LitElement {
</ha-formfield>
`
: ""}
`
: ""}
${this.snapshotType === "partial"
? html`
${foldersSection?.templates.length
? html`
<ha-formfield
.label=${html`<supervisor-formfield-label
.label=${this.supervisor.localize("snapshot.folders")}
.label=${this._localize("folders")}
.iconPath=${mdiFolder}
>
</supervisor-formfield-label>`}
@@ -207,7 +215,7 @@ export class SupervisorSnapshotContent extends LitElement {
? html`
<ha-formfield
.label=${html`<supervisor-formfield-label
.label=${this.supervisor.localize("snapshot.addons")}
.label=${this._localize("addons")}
.iconPath=${mdiPuzzle}
>
</supervisor-formfield-label>`}
@@ -223,29 +231,44 @@ 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
.label=${this.supervisor.localize("snapshot.password_protection")}
class="password"
.label=${this._localize("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.supervisor.localize("snapshot.password")}
.label=${this._localize("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>`
: ""}
`
: ""}
`;
@@ -253,21 +276,24 @@ export class SupervisorSnapshotContent extends LitElement {
static get styles(): CSSResultGroup {
return css`
ha-checkbox {
--mdc-checkbox-touch-target-size: 16px;
.partial-picker ha-formfield {
display: block;
margin: 4px 12px 8px 0;
}
ha-formfield {
display: contents;
.partial-picker ha-checkbox {
--mdc-checkbox-touch-target-size: 32px;
}
.partial-picker {
display: block;
margin: 0px -6px;
}
supervisor-formfield-label {
display: inline-flex;
align-items: center;
}
paper-input[type="password"] {
display: block;
margin: 4px 0 4px 16px;
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 16px 0;
}
.details {
color: var(--secondary-text-color);
@@ -275,13 +301,15 @@ export class SupervisorSnapshotContent extends LitElement {
.section-content {
display: flex;
flex-direction: column;
margin-left: 16px;
margin-left: 30px;
}
.security {
margin-top: 16px;
ha-formfield.password {
display: block;
margin: 0 -14px -16px;
}
.snapshot-types {
display: flex;
margin-left: -13px;
}
.sub-header {
margin-top: 8px;
@@ -300,6 +328,9 @@ export class SupervisorSnapshotContent extends LitElement {
if (this.snapshotHasPassword) {
data.password = this.snapshotPassword;
if (!this.snapshot) {
data.confirm_password = this.confirmSnapshotPassword;
}
}
if (this.snapshotType === "full") {
@@ -331,7 +362,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;
@@ -341,6 +372,7 @@ 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`

View File

@@ -161,6 +161,7 @@ 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}`,

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ class HassioRegistriesDialog extends LitElement {
return html`
<ha-dialog
.open=${this._opened}
@closing=${this.closeDialog}
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
@@ -244,9 +244,6 @@ class HassioRegistriesDialog extends LitElement {
mwc-list-item span[slot="secondary"] {
color: var(--secondary-text-color);
}
ha-paper-dropdown-menu {
display: block;
}
`,
];
}

View File

@@ -67,7 +67,7 @@ class HassioRepositoriesDialog extends LitElement {
return html`
<ha-dialog
.open=${this._opened}
@closing=${this.closeDialog}
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
@@ -150,9 +150,6 @@ class HassioRepositoriesDialog extends LitElement {
mwc-button {
margin-left: 8px;
}
ha-paper-dropdown-menu {
display: block;
}
ha-circular-progress {
display: block;
margin: 32px;

View File

@@ -48,7 +48,8 @@ class HassioCreateSnapshotDialog extends LitElement {
return html`
<ha-dialog
open
@closing=${this.closeDialog}
scrimClickAction
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this._dialogParams.supervisor.localize("snapshot.create_snapshot")
@@ -94,16 +95,25 @@ class HassioCreateSnapshotDialog extends LitElement {
this._creatingSnapshot = true;
this._error = "";
if (
this._snapshotContent.snapshotHasPassword &&
!this._snapshotContent.snapshotPassword.length
) {
if (snapshotDetails.password && !snapshotDetails.password.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") {

View File

@@ -1,12 +1,13 @@
import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js";
import { mdiClose, 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 { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-svg-icon";
import { getSignedPath } from "../../../../src/data/auth";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
@@ -21,6 +22,7 @@ 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";
@@ -63,15 +65,26 @@ class HassioSnapshotDialog
return html`
<ha-dialog
open
@closing=${this.closeDialog}
.heading=${createCloseHeading(this.hass, this._computeName)}
scrimClickAction
@closed=${this.closeDialog}
.heading=${true}
>
<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>` : ""}
@@ -84,18 +97,20 @@ class HassioSnapshotDialog
Restore
</mwc-button>
<ha-button-menu
fixed
slot="primaryAction"
@action=${this._handleMenuAction}
@closing=${(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>
${!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-dialog>
`;
}
@@ -112,6 +127,12 @@ 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;
}
`,
];
}
@@ -286,13 +307,11 @@ class HassioSnapshotDialog
}
}
const name = this._computeName.replace(/[^a-z0-9]+/gi, "_");
const a = document.createElement("a");
a.href = signedPath.path;
a.download = `Hass_io_${name}.tar`;
this.shadowRoot!.appendChild(a);
a.click();
this.shadowRoot!.removeChild(a);
fileDownload(
this,
signedPath.path,
`home_assistant_snapshot_${slugify(this._computeName)}.tar`
);
}
private get _computeName() {

View File

@@ -1,4 +1,5 @@
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 {
@@ -6,6 +7,7 @@ export interface HassioSnapshotDialogParams {
onDelete?: () => void;
onboarding?: boolean;
supervisor?: Supervisor;
localize?: LocalizeFunc;
}
export const showHassioSnapshotDialog = (

View File

@@ -2,19 +2,32 @@ 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 {
@@ -22,12 +35,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;
@@ -36,14 +49,17 @@ 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 });
}
@@ -56,7 +72,7 @@ class DialogSupervisorUpdate extends LitElement {
}
protected render(): TemplateResult {
if (!this._dialogParams) {
if (!this._dialogParams || !this._frontendPrefrences) {
return html``;
}
return html`
@@ -82,6 +98,16 @@ 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"
@@ -94,12 +120,6 @@ 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")}
@@ -133,12 +153,27 @@ class DialogSupervisorUpdate extends LitElement {
`;
}
private _toggleSnapshot() {
this._createSnapshot = !this._createSnapshot;
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 async _update() {
if (this._createSnapshot) {
if (
snapshot_before_update(
this._dialogParams!.slug,
this._frontendPrefrences!
)
) {
this._action = "snapshot";
try {
await createHassioPartialSnapshot(

View File

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

View File

@@ -103,27 +103,25 @@ export class HassioMain extends SupervisorBaseElement {
private _applyTheme() {
let themeName: string;
let themeSettings:
| Partial<HomeAssistant["selectedThemeSettings"]>
| undefined;
let themeSettings: Partial<HomeAssistant["selectedTheme"]> | undefined;
if (atLeastVersion(this.hass.config.version, 0, 114)) {
themeName =
this.hass.selectedThemeSettings?.theme ||
this.hass.selectedTheme?.theme ||
(this.hass.themes.darkMode && this.hass.themes.default_dark_theme
? this.hass.themes.default_dark_theme!
: this.hass.themes.default_theme);
themeSettings = this.hass.selectedThemeSettings;
themeSettings = this.hass.selectedTheme;
if (themeSettings?.dark === undefined) {
themeSettings = {
...this.hass.selectedThemeSettings,
...this.hass.selectedTheme,
dark: this.hass.themes.darkMode,
};
}
} else {
themeName =
((this.hass.selectedThemeSettings as unknown) as string) ||
((this.hass.selectedTheme as unknown) as string) ||
this.hass.themes.default_theme;
}

View File

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

View File

@@ -1,15 +1,17 @@
import "@material/mwc-button";
import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical, mdiPlus } from "@mdi/js";
import { mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import relativeTime from "../../../src/common/datetime/relative_time";
@@ -17,18 +19,25 @@ 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 } from "../../../src/dialogs/generic/show-dialog-box";
import {
showAlertDialog,
showConfirmationDialog,
} 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";
@@ -49,10 +58,15 @@ export class HassioSnapshots extends LitElement {
@property({ type: Boolean }) public isWide!: boolean;
private _firstUpdatedCalled = false;
@state() private _selectedSnapshots: string[] = [];
@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) {
@@ -153,7 +167,9 @@ 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
@@ -176,6 +192,45 @@ 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}
@@ -199,6 +254,12 @@ export class HassioSnapshots extends LitElement {
}
}
private _handleSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selectedSnapshots = ev.detail.value;
}
private _showUploadSnapshotDialog() {
showSnapshotUploadDialog(this, {
showSnapshot: (slug: string) =>
@@ -216,6 +277,35 @@ 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, {
@@ -244,7 +334,45 @@ export class HassioSnapshots extends LitElement {
}
static get styles(): CSSResultGroup {
return [haStyle, hassioStyle];
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;
}
`,
];
}
}

View File

@@ -164,6 +164,7 @@ 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}`,

View File

@@ -2,7 +2,6 @@ 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 { safeDump } from "js-yaml";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -41,8 +40,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")
@@ -229,20 +228,19 @@ class HassioHostInfo extends LitElement {
}
private async _showHardware(): Promise<void> {
let hardware;
try {
const content = await fetchHassioHardwareInfo(this.hass);
showHassioMarkdownDialog(this, {
title: this.supervisor.localize("system.host.hardware"),
content: `<pre>${safeDump(content, { indent: 2 })}</pre>`,
});
hardware = await fetchHassioHardwareInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
await 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> {

View File

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

View File

@@ -16,13 +16,13 @@
"lint:lit": "lit-analyzer \"**/src/**/*.ts\" --format markdown --outFile result.md",
"lint": "yarn run lint:eslint && yarn run lint:prettier && yarn run lint:types",
"format": "yarn run format:eslint && yarn run format:prettier",
"mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --opts test-mocha/mocha.opts",
"mocha": "ts-mocha -p test-mocha/tsconfig.test.json \"test-mocha/**/*.ts\"",
"test": "yarn run lint && yarn run mocha"
},
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0",
"dependencies": {
"@braintree/sanitize-url": "^5.0.0",
"@braintree/sanitize-url": "^5.0.1",
"@codemirror/commands": "^0.18.0",
"@codemirror/gutter": "^0.18.0",
"@codemirror/highlight": "^0.18.0",
@@ -35,8 +35,8 @@
"@codemirror/text": "^0.18.0",
"@codemirror/view": "^0.18.0",
"@formatjs/intl-getcanonicallocales": "^1.5.10",
"@formatjs/intl-locale": "^2.4.24",
"@formatjs/intl-pluralrules": "^4.0.18",
"@formatjs/intl-locale": "^2.4.28",
"@formatjs/intl-pluralrules": "^4.0.22",
"@fullcalendar/common": "5.1.0",
"@fullcalendar/core": "5.1.0",
"@fullcalendar/daygrid": "5.1.0",
@@ -66,12 +66,9 @@
"@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",
@@ -101,26 +98,26 @@
"@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "^2.9.4",
"chartjs-chart-timeline": "^0.4.0",
"comlink": "^4.3.0",
"comlink": "^4.3.1",
"core-js": "^3.6.5",
"cropperjs": "^1.5.7",
"cropperjs": "^1.5.11",
"deep-clone-simple": "^1.1.1",
"deep-freeze": "^0.0.1",
"fecha": "^4.2.0",
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
"hls.js": "^1.0.3",
"hls.js": "^1.0.5",
"home-assistant-js-websocket": "^5.10.0",
"idb-keyval": "^3.2.0",
"intl-messageformat": "^9.6.13",
"js-yaml": "^3.13.1",
"idb-keyval": "^5.0.5",
"intl-messageformat": "^9.6.16",
"js-yaml": "^4.1.0",
"leaflet": "^1.7.1",
"leaflet-draw": "^1.0.4",
"lit": "^2.0.0-rc.2",
"lit-vaadin-helpers": "^0.1.3",
"marked": "2.0.0",
"marked": "^2.0.5",
"mdn-polyfills": "^5.16.0",
"memoize-one": "^5.0.2",
"memoize-one": "^5.2.1",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "^0.3.1",
"punycode": "^2.1.1",
@@ -130,12 +127,12 @@
"roboto-fontface": "^0.10.0",
"sortablejs": "^1.10.2",
"superstruct": "^0.15.2",
"tinykeys": "^1.1.1",
"tinykeys": "^1.1.3",
"tsparticles": "^1.19.2",
"unfetch": "^4.1.0",
"vis-data": "^7.1.1",
"vis-data": "^7.1.2",
"vis-network": "^8.5.4",
"vue": "^2.6.11",
"vue": "^2.6.12",
"vue2-daterange-picker": "^0.5.1",
"web-animations-js": "^2.3.2",
"workbox-cacheable-response": "^6.1.5",
@@ -147,7 +144,7 @@
"xss": "^1.0.9"
},
"devDependencies": {
"@babel/core": "^7.14.0",
"@babel/core": "^7.14.3",
"@babel/plugin-external-helpers": "^7.12.13",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-decorators": "^7.13.15",
@@ -156,7 +153,7 @@
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/preset-env": "^7.14.0",
"@babel/preset-env": "^7.14.2",
"@babel/preset-typescript": "^7.13.0",
"@koa/cors": "^3.1.0",
"@open-wc/dev-server-hmr": "^0.0.2",
@@ -165,16 +162,13 @@
"@rollup/plugin-json": "^4.0.3",
"@rollup/plugin-node-resolve": "^7.1.3",
"@rollup/plugin-replace": "^2.3.2",
"@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^5.0.11",
"@types/chromecast-caf-receiver": "5.0.12",
"@types/chromecast-caf-sender": "^1.0.3",
"@types/js-yaml": "^3.12.1",
"@types/js-yaml": "^4.0.1",
"@types/leaflet": "^1.7.0",
"@types/leaflet-draw": "^1.0.3",
"@types/marked": "^1.2.2",
"@types/memoize-one": "4.1.0",
"@types/mocha": "^7.0.2",
"@types/resize-observer-browser": "^0.1.3",
"@types/marked": "^2.0.3",
"@types/mocha": "^8.2.2",
"@types/sortablejs": "^1.10.6",
"@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^4.22.0",
@@ -182,7 +176,7 @@
"@web/dev-server": "^0.0.24",
"@web/dev-server-rollup": "^0.2.11",
"babel-loader": "^8.1.0",
"chai": "^4.2.0",
"chai": "^4.3.4",
"cpx": "^1.5.0",
"del": "^4.0.0",
"eslint": "^7.25.0",
@@ -196,7 +190,7 @@
"eslint-plugin-wc": "^1.3.0",
"fancy-log": "^1.3.3",
"fs-extra": "^7.0.1",
"gulp": "^4.0.0",
"gulp": "^4.0.2",
"gulp-foreach": "^0.1.0",
"gulp-json-transform": "^0.4.6",
"gulp-merge-json": "^1.3.1",
@@ -210,7 +204,7 @@
"magic-string": "^0.25.7",
"map-stream": "^0.0.7",
"merge-stream": "^1.0.1",
"mocha": "^7.2.0",
"mocha": "^8.4.0",
"object-hash": "^2.0.3",
"open": "^7.0.4",
"prettier": "^2.0.4",
@@ -220,13 +214,13 @@
"rollup-plugin-string": "^3.0.0",
"rollup-plugin-terser": "^5.3.0",
"rollup-plugin-visualizer": "^4.0.4",
"serve": "^11.3.0",
"sinon": "^7.3.1",
"serve": "^11.3.2",
"sinon": "^11.0.0",
"source-map-url": "^0.4.0",
"systemjs": "^6.3.2",
"terser-webpack-plugin": "^5.1.1",
"terser-webpack-plugin": "^5.1.2",
"ts-lit-plugin": "^1.2.1",
"ts-mocha": "^7.0.0",
"ts-mocha": "^8.0.0",
"typescript": "^4.2.4",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",

View File

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

View File

@@ -31,7 +31,7 @@ export const applyThemesOnElement = (
element,
themes: HomeAssistant["themes"],
selectedTheme?: string,
themeSettings?: Partial<HomeAssistant["selectedThemeSettings"]>
themeSettings?: Partial<HomeAssistant["selectedTheme"]>
) => {
let cacheKey = selectedTheme;
let themeRules: Partial<ThemeVars> = {};
@@ -39,7 +39,7 @@ export const applyThemesOnElement = (
if (themeSettings) {
if (themeSettings.dark) {
cacheKey = `${cacheKey}__dark`;
themeRules = darkStyles;
themeRules = { ...darkStyles };
}
if (selectedTheme === "default") {

View File

@@ -6,8 +6,7 @@ export type LeafletDrawModuleType = typeof import("leaflet-draw");
export const setupLeafletMap = async (
mapElement: HTMLElement,
darkMode?: boolean,
draw = false
darkMode?: boolean
): Promise<[Map, LeafletModuleType, TileLayer]> => {
if (!mapElement.parentNode) {
throw new Error("Cannot setup Leaflet map on disconnected element");
@@ -17,10 +16,6 @@ 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");

View File

@@ -1,14 +1,7 @@
/* eslint-disable */
// @ts-ignore
export const SpeechRecognition =
// @ts-ignore
window.SpeechRecognition || window.webkitSpeechRecognition;
// @ts-ignore
export const SpeechGrammarList =
// @ts-ignore
window.SpeechGrammarList || window.webkitSpeechGrammarList;
// @ts-ignore
export const SpeechRecognitionEvent =
// @ts-ignore
// @ts-expect-error
window.SpeechRecognitionEvent || window.webkitSpeechRecognitionEvent;
/* eslint-enable */

View File

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

View File

@@ -1,9 +1,16 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMagnify } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
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 "../../components/ha-svg-icon";
import { fireEvent } from "../dom/fire_event";
@@ -27,18 +34,11 @@ 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,6 +62,17 @@ 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) });
}

View File

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

View File

@@ -92,7 +92,7 @@ function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean {
return word[pos] !== wordLow[pos];
}
function isPatternInWord(
export function isPatternInWord(
patternLow: string,
patternPos: number,
patternLen: number,
@@ -121,7 +121,7 @@ enum Arrow {
}
/**
* An array representating a fuzzy match.
* An array representing a fuzzy match.
*
* 0. the score
* 1. the offset at which matching started

View File

@@ -5,7 +5,7 @@ import { fuzzyScore } from "./filter";
* in that order, allowing for skipping. Ex: "chdr" exists in "chandelier")
*
* @param {string} filter - Sequence of letters to check for
* @param {string} word - Word to check for sequence
* @param {ScorableTextItem} item - Item against whose strings will be checked
*
* @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
*/

View File

@@ -0,0 +1,11 @@
// https://regex101.com/r/kc5C14/2
const regExpString = "^\\d{4}-(0[1-9]|1[0-2])-([12]\\d|0[1-9]|3[01])";
const regExp = new RegExp(regExpString + "$");
// 2nd expression without the "end of string" enforced, so it can be used
// to just verify the start of a string and then based on that result e.g.
// check for a full timestamp string efficiently.
const regExpNoStringEnd = new RegExp(regExpString);
export const isDate = (input: string, allowCharsAfterDate = false): boolean =>
allowCharsAfterDate ? regExpNoStringEnd.test(input) : regExp.test(input);

View File

@@ -0,0 +1,11 @@
// https://stackoverflow.com/a/14322189/1947205
// Changes:
// 1. Do not allow a plus or minus at the start.
// 2. Enforce that we have a "T" or a blank after the date portion
// to ensure we have a timestamp and not only a date.
// 3. Disallow dates based on week number.
// 4. Disallow dates only consisting of a year.
// https://regex101.com/r/kc5C14/3
const regexp = /^\d{4}-(0[1-9]|1[0-2])-([12]\d|0[1-9]|3[01])[T| ](((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)(\8[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)$/;
export const isTimestamp = (input: string): boolean => regexp.test(input);

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { shouldPolyfill } from "@formatjs/intl-pluralrules/should-polyfill";
import { shouldPolyfill } from "@formatjs/intl-pluralrules/lib/should-polyfill";
import IntlMessageFormat from "intl-messageformat";
import { Resources } from "../../types";
@@ -86,11 +86,15 @@ export const computeLocalize = async (
| undefined;
if (!translatedMessage) {
translatedMessage = new IntlMessageFormat(
translatedValue,
language,
formats
);
try {
translatedMessage = new IntlMessageFormat(
translatedValue,
language,
formats
);
} catch (err) {
return "Translation error: " + err.message;
}
cache._localizationCache[messageKey] = translatedMessage;
}

View File

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

View File

@@ -0,0 +1,10 @@
export const promiseTimeout = (ms: number, promise: Promise<any>) => {
const timeout = new Promise((_resolve, reject) => {
setTimeout(() => {
reject(`Timed out in ${ms} ms.`);
}, ms);
});
// Returns a race between our timeout and the passed in promise
return Promise.race([promise, timeout]);
};

View File

@@ -1,4 +1,4 @@
import { Layout1d, scroll } from "@lit-labs/virtualizer";
import { Layout1d, scroll } from "../../resources/lit-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 + 57}px`
? `${(this._filteredData.length || 1) * 53 + 53}px`
: `calc(100% - ${this._headerHeight}px)`,
})}
>
@@ -340,11 +340,10 @@ 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 "";
return html``;
}
if (row.append) {
return html`
@@ -920,13 +919,11 @@ 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:not(.auto-height) .scroller {
overflow: auto;
.mdc-data-table__table.auto-height .scroller {
overflow-y: hidden !important;
}
.grows {
flex-grow: 1;

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,14 @@
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
import hassAttributeUtil, {
formatAttributeName,
formatAttributeValue,
} from "../util/hass-attributes-util";
import "./ha-expansion-panel";
let jsYamlPromise: Promise<typeof import("js-yaml")>;
@customElement("ha-attributes")
class HaAttributes extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -56,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>
`
: ""}
`;
}
@@ -93,6 +91,7 @@ class HaAttributes extends LitElement {
.attribution {
color: var(--secondary-text-color);
text-align: center;
margin-top: 16px;
}
pre {
font-family: inherit;
@@ -124,38 +123,7 @@ class HaAttributes extends LitElement {
return "-";
}
const value = this.stateObj.attributes[attribute];
return this.formatAttributeValue(value);
}
private formatAttributeValue(value: any): string | TemplateResult {
if (value === null) {
return "-";
}
// YAML handling
if (
(Array.isArray(value) && value.some((val) => val instanceof Object)) ||
(!Array.isArray(value) && value instanceof Object)
) {
if (!jsYamlPromise) {
jsYamlPromise = import("js-yaml");
}
const yaml = jsYamlPromise.then((jsYaml) => jsYaml.safeDump(value));
return html` <pre>${until(yaml, "")}</pre> `;
}
// URL handling
if (typeof value === "string" && value.startsWith("http")) {
try {
// If invalid URL, exception will be raised
const url = new URL(value);
if (url.protocol === "http:" || url.protocol === "https:")
return html`<a target="_blank" rel="noreferrer" href="${value}"
>${value}</a
>`;
} catch (_) {
// Nothing to do here
}
}
return Array.isArray(value) ? value.join(", ") : value;
return formatAttributeValue(this.hass, value);
}
private expandedChanged(ev) {

View File

@@ -1,4 +1,5 @@
import { CircularProgress } from "@material/mwc-circular-progress";
import { CSSResultGroup, css } from "lit";
import { customElement, property } from "lit/decorators";
@customElement("ha-circular-progress")
@@ -41,6 +42,17 @@ export class HaCircularProgress extends CircularProgress {
public get indeterminate() {
return this.active;
}
static get styles(): CSSResultGroup {
return [
super.styles,
css`
:host {
overflow: hidden;
}
`,
];
}
}
declare global {

View File

@@ -23,8 +23,8 @@ class HaCoverControls extends LitElement {
@state() private _entityObj?: CoverEntity;
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
public willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (changedProperties.has("stateObj")) {
this._entityObj = new CoverEntity(this.hass, this.stateObj);

View File

@@ -22,8 +22,8 @@ class HaCoverTiltControls extends LitElement {
@state() private _entityObj?: CoverEntity;
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
public willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (changedProperties.has("stateObj")) {
this._entityObj = new CoverEntity(this.hass, this.stateObj);

View File

@@ -14,12 +14,17 @@ 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 name="header">${this.header}</slot>
<slot class="header" name="header">
${this.header}
<slot class="secondary" name="secondary">${this.secondary}</slot>
</slot>
<ha-svg-icon
.path=${mdiChevronDown}
class="summary-icon ${classMap({ expanded: this.expanded })}"
@@ -106,6 +111,16 @@ class HaExpansionPanel extends LitElement {
.container.expanded {
height: auto;
}
.header {
display: block;
}
.secondary {
display: block;
color: var(--secondary-text-color);
font-size: 12px;
}
`;
}
}

View File

@@ -7,14 +7,14 @@ import { afterNextRender } from "../common/util/render-status";
import { FrontendLocaleData } from "../data/translation";
import { 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;

View File

@@ -13,6 +13,11 @@ 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;
@@ -39,7 +44,7 @@ class HaHLSPlayer extends LitElement {
@state() private _attached = false;
private _hlsPolyfillInstance?: HlsType;
private _hlsPolyfillInstance?: HlsLite;
private _useExoPlayer = false;
@@ -103,7 +108,8 @@ class HaHLSPlayer extends LitElement {
const useExoPlayerPromise = this._getUseExoPlayer();
const masterPlaylistPromise = fetch(this.url);
const Hls = (await import("hls.js")).default;
const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min.js"))
.default;
let hlsSupported = Hls.isSupported();
if (!hlsSupported) {
@@ -182,7 +188,7 @@ class HaHLSPlayer extends LitElement {
url: string
) {
const hls = new Hls({
liveBackBufferLength: 60,
backBufferLength: 60,
fragLoadingTimeOut: 30000,
manifestLoadingTimeOut: 30000,
levelLoadingTimeOut: 30000,

View File

@@ -125,6 +125,7 @@ export class HaIcon extends LitElement {
databaseIcon = await getIcon(iconName);
} catch (_err) {
// Firefox in private mode doesn't support IDB
// iOS Safari sometimes doesn't open the DB
databaseIcon = undefined;
}

View File

@@ -0,0 +1,179 @@
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
CSSResultGroup,
html,
nothing,
LitElement,
TemplateResult,
} from "lit";
import { customElement, state, property } from "lit/decorators";
import {
Adapter,
NetworkConfig,
IPv6ConfiguredAddress,
IPv4ConfiguredAddress,
} from "../data/network";
import { fireEvent } from "../common/dom/fire_event";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
import "./ha-checkbox";
import type { HaCheckbox } from "./ha-checkbox";
import "./ha-settings-row";
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,
])}`;
const format_auto_detected_interfaces = (
adapters: Adapter[]
): Array<TemplateResult | string> =>
adapters.map((adapter) =>
adapter.auto
? html`${adapter.name}
(${format_addresses([...adapter.ipv4, ...adapter.ipv6])})`
: ""
);
declare global {
interface HASSDomEvents {
"network-config-changed": { configured_adapters: string[] };
}
}
@customElement("ha-network")
export class HaNetwork extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public networkConfig?: NetworkConfig;
@state() private _expanded?: boolean;
protected render(): TemplateResult {
if (this.networkConfig === undefined) {
return html``;
}
const configured_adapters = this.networkConfig.configured_adapters || [];
return html`
<ha-settings-row>
<span slot="prefix">
<ha-checkbox
id="auto_configure"
@change=${this._handleAutoConfigureCheckboxClick}
.checked=${!configured_adapters.length}
name="auto_configure"
>
</ha-checkbox>
</span>
<span slot="heading" data-for="auto_configure"> Auto Configure </span>
<span slot="description" data-for="auto_configure">
Detected:
${format_auto_detected_interfaces(this.networkConfig.adapters)}
</span>
</ha-settings-row>
${configured_adapters.length || this._expanded
? this.networkConfig.adapters.map(
(adapter) =>
html`<ha-settings-row>
<span slot="prefix">
<ha-checkbox
id=${adapter.name}
@change=${this._handleAdapterCheckboxClick}
.checked=${configured_adapters.includes(adapter.name)}
.adapter=${adapter.name}
name=${adapter.name}
>
</ha-checkbox>
</span>
<span slot="heading">
Adapter: ${adapter.name}
${adapter.default
? html`<ha-icon .icon="hass:star"></ha-icon> (Default)`
: ""}
</span>
<span slot="description">
${format_addresses([...adapter.ipv4, ...adapter.ipv6])}
</span>
</ha-settings-row>`
)
: ""}
`;
}
private _handleAutoConfigureCheckboxClick(ev: Event) {
const checkbox = ev.currentTarget as HaCheckbox;
if (this.networkConfig === undefined) {
return;
}
let configured_adapters = [...this.networkConfig.configured_adapters];
if (checkbox.checked) {
this._expanded = false;
configured_adapters = [];
} else {
this._expanded = true;
for (const adapter of this.networkConfig.adapters) {
if (adapter.default) {
configured_adapters = [adapter.name];
break;
}
}
}
fireEvent(this, "network-config-changed", {
configured_adapters: configured_adapters,
});
}
private _handleAdapterCheckboxClick(ev: Event) {
const checkbox = ev.currentTarget as HaCheckbox;
const adapter_name = (checkbox as any).name;
if (this.networkConfig === undefined) {
return;
}
const configured_adapters = [...this.networkConfig.configured_adapters];
if (checkbox.checked) {
configured_adapters.push(adapter_name);
} else {
const index = configured_adapters.indexOf(adapter_name, 0);
configured_adapters.splice(index, 1);
}
fireEvent(this, "network-config-changed", {
configured_adapters: configured_adapters,
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.error {
color: var(--error-color);
}
ha-settings-row {
padding: 0;
}
span[slot="heading"],
span[slot="description"] {
cursor: pointer;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-network": HaNetwork;
}
}

View File

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

View File

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

View File

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

View File

@@ -3,10 +3,8 @@ import {
DivIcon,
DragEndEvent,
LatLng,
Map,
Marker,
MarkerOptions,
TileLayer,
} from "leaflet";
import {
css,
@@ -16,15 +14,13 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import {
LeafletModuleType,
replaceTileLayer,
setupLeafletMap,
} from "../../common/dom/setup-leaflet-map";
import { defaultRadiusColor } from "../../data/zone";
import { HomeAssistant } from "../../types";
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
import type { HomeAssistant } from "../../types";
import "./ha-map";
import type { HaMap } from "./ha-map";
declare global {
// for fire event
@@ -51,38 +47,40 @@ export interface MarkerLocation {
export class HaLocationsEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public locations?: MarkerLocation[];
@property({ attribute: false }) public locations?: MarkerLocation[];
public fitZoom = 16;
@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;
// eslint-disable-next-line
private Leaflet?: LeafletModuleType;
// eslint-disable-next-line
private _leafletMap?: Map;
constructor() {
super();
private _tileLayer?: TileLayer;
private _locationMarkers?: { [key: string]: Marker | Circle };
private _circles: Record<string, Circle> = {};
import("leaflet").then((module) => {
import("leaflet-draw").then(() => {
this.Leaflet = module.default as LeafletModuleType;
this._updateMarkers();
this.updateComplete.then(() => this.fitMap());
});
});
}
public fitMap(): void {
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));
this.map.fitMap();
}
public fitMarker(id: string): void {
if (!this._leafletMap || !this._locationMarkers) {
if (!this.map.leafletMap || !this._locationMarkers) {
return;
}
const marker = this._locationMarkers[id];
@@ -90,29 +88,44 @@ export class HaLocationsEditor extends LitElement {
return;
}
if ("getBounds" in marker) {
this._leafletMap.fitBounds(marker.getBounds());
this.map.leafletMap.fitBounds(marker.getBounds());
(marker as Circle).bringToFront();
} else {
const circle = this._circles[id];
if (circle) {
this._leafletMap.fitBounds(circle.getBounds());
this.map.leafletMap.fitBounds(circle.getBounds());
} else {
this._leafletMap.setView(marker.getLatLng(), this.fitZoom);
this.map.leafletMap.setView(marker.getLatLng(), this.zoom);
}
}
}
protected render(): TemplateResult {
return html` <div id="map"></div> `;
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>`;
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._initMap();
}
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 updated(changedProps: PropertyValues): void {
super.updated(changedProps);
public willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
// Still loading.
if (!this.Leaflet) {
@@ -122,37 +135,6 @@ 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) {
@@ -189,21 +171,18 @@ export class HaLocationsEditor extends LitElement {
}
private _updateMarkers(): void {
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) {
this._circles = {};
this._locationMarkers = undefined;
return;
}
this._locationMarkers = {};
const locationMarkers = {};
const circles = {};
const defaultZoneRadiusColor = getComputedStyle(this).getPropertyValue(
"--accent-color"
);
this.locations.forEach((location: MarkerLocation) => {
let icon: DivIcon | undefined;
@@ -228,45 +207,46 @@ export class HaLocationsEditor extends LitElement {
const circle = this.Leaflet!.circle(
[location.latitude, location.longitude],
{
color: location.radius_color || defaultRadiusColor,
color: location.radius_color || defaultZoneRadiusColor,
radius: location.radius,
}
);
circle.addTo(this._leafletMap!);
if (location.radius_editable || location.location_editable) {
// @ts-ignore
circle.editing.enable();
// @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;
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;
} else {
this._circles[location.id] = circle;
circles[location.id] = circle;
}
}
if (
@@ -275,6 +255,7 @@ export class HaLocationsEditor extends LitElement {
) {
const options: MarkerOptions = {
title: location.name,
draggable: location.location_editable,
};
if (icon) {
@@ -293,13 +274,14 @@ export class HaLocationsEditor extends LitElement {
"click",
// @ts-ignore
(ev: MouseEvent) => this._markerClicked(ev)
)
.addTo(this._leafletMap!);
);
(marker as any).id = location.id;
this._locationMarkers![location.id] = marker;
locationMarkers[location.id] = marker;
}
});
this._circles = circles;
this._locationMarkers = locationMarkers;
}
static get styles(): CSSResultGroup {
@@ -308,23 +290,9 @@ export class HaLocationsEditor extends LitElement {
display: block;
height: 300px;
}
#map {
ha-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;
}
`;
}
}

View File

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

View File

@@ -45,7 +45,6 @@ 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 {

View File

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

View File

@@ -194,7 +194,10 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
invertOnOff,
]);
}
datasets.push({ data: dataRow, entity_id: stateInfo.entity_id });
datasets.push({
data: dataRow,
entity_id: stateInfo.entity_id,
});
labels.push(entityDisplay);
});
@@ -233,6 +236,14 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
major: {
fontStyle: "bold",
},
sampleSize: 5,
autoSkipPadding: 50,
maxRotation: 0,
},
categoryPercentage: undefined,
barPercentage: undefined,
time: {
format: undefined,
},
},
],
@@ -242,10 +253,17 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
yaxe.maxWidth = yaxe.chart.width * 0.18;
},
position: this._computeRTL ? "right" : "left",
categoryPercentage: undefined,
barPercentage: undefined,
time: { format: undefined },
},
],
},
},
datasets: {
categoryPercentage: 0.8,
barPercentage: 0.9,
},
data: {
labels: labels,
datasets: datasets,

View File

@@ -1,4 +1,11 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
@@ -32,13 +39,13 @@ class UserBadge extends LitElement {
private _personEntityId?: string;
protected updated(changedProps) {
super.updated(changedProps);
public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
if (changedProps.has("user")) {
this._getPersonPicture();
return;
}
const oldHass = changedProps.get("hass");
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (
this._personEntityId &&
oldHass &&

View File

@@ -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 interface ConfigEntryMutableParams {
title: string;
}
export interface ConfigEntrySystemOptions {
disable_new_entities: boolean;
}
export type ConfigEntryMutableParams = Partial<
Pick<
ConfigEntry,
"title" | "pref_disable_new_entities" | "pref_disable_polling"
>
>;
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: Partial<ConfigEntryMutableParams>
updatedValues: ConfigEntryMutableParams
) =>
hass.callWS<ConfigEntry>({
hass.callWS<{ require_restart: boolean; config_entry: ConfigEntry }>({
type: "config_entries/update",
entry_id: configEntryId,
...updatedValues,
@@ -51,13 +51,15 @@ 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<{
require_restart: boolean;
}>({
hass.callWS<DisableConfigEntryResult>({
type: "config_entries/disable",
entry_id: configEntryId,
disabled_by: "user",
@@ -71,23 +73,3 @@ 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,
});

View File

@@ -212,13 +212,15 @@ export const setHassioAddonOption = async (
export const validateHassioAddonOption = async (
hass: HomeAssistant,
slug: string
slug: string,
data?: any
): 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,
});
}

View File

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

View File

@@ -41,6 +41,7 @@ export interface HassioSnapshotDetail extends HassioSnapshot {
export interface HassioFullSnapshotCreateParams {
name: string;
password?: string;
confirm_password?: string;
}
export interface HassioPartialSnapshotCreateParams
extends HassioFullSnapshotCreateParams {
@@ -130,6 +131,21 @@ 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

View File

@@ -1,4 +1,5 @@
import { clear, get, set, Store } from "idb-keyval";
import { clear, get, set, createStore, promisifyRequest } from "idb-keyval";
import { promiseTimeout } from "../common/util/promise-timeout";
import { iconMetadata } from "../resources/icon-metadata";
import { IconMeta } from "../types";
@@ -10,45 +11,44 @@ export interface Chunks {
[key: string]: Promise<Icons>;
}
export const iconStore = new Store("hass-icon-db", "mdi-icon-store");
export const iconStore = createStore("hass-icon-db", "mdi-icon-store");
export const MDI_PREFIXES = ["mdi", "hass", "hassio", "hademo"];
let toRead: Array<[string, (string) => void, () => void]> = [];
let toRead: Array<
[string, (iconPath: string | undefined) => void, (e: any) => void]
> = [];
// Queue up as many icon fetches in 1 transaction
export const getIcon = (iconName: string) =>
new Promise<string>((resolve, reject) => {
new Promise<string | undefined>((resolve, reject) => {
toRead.push([iconName, resolve, reject]);
if (toRead.length > 1) {
return;
}
const results: Array<[(string) => void, IDBRequest]> = [];
iconStore
._withIDBStore("readonly", (store) => {
for (const [iconName_, resolve_] of toRead) {
results.push([resolve_, store.get(iconName_)]);
promiseTimeout(
1000,
iconStore("readonly", (store) => {
for (const [iconName_, resolve_, reject_] of toRead) {
promisifyRequest<string | undefined>(store.get(iconName_))
.then((icon) => resolve_(icon))
.catch((e) => reject_(e));
}
toRead = [];
})
.then(() => {
for (const [resolve_, request] of results) {
resolve_(request.result);
}
})
.catch(() => {
// Firefox in private mode doesn't support IDB
for (const [, , reject_] of toRead) {
reject_();
}
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 = [];
});
});
export const findIconChunk = (icon): string => {
export const findIconChunk = (icon: string): string => {
let lastChunk: IconMeta;
for (const chunk of iconMetadata.parts) {
if (chunk.start !== undefined && icon < chunk.start) {
@@ -63,7 +63,7 @@ export const writeCache = async (chunks: Chunks) => {
const keys = Object.keys(chunks);
const iconsSets: Icons[] = await Promise.all(Object.values(chunks));
// We do a batch opening the store just once, for (considerable) performance
iconStore._withIDBStore("readwrite", (store) => {
iconStore("readwrite", (store) => {
iconsSets.forEach((icons, idx) => {
Object.entries(icons).forEach(([name, path]) => {
store.put(path, name);
@@ -73,14 +73,13 @@ export const writeCache = async (chunks: Chunks) => {
});
};
export const checkCacheVersion = () => {
get("_version", iconStore).then((version) => {
if (!version) {
set("_version", iconMetadata.version, iconStore);
} else if (version !== iconMetadata.version) {
clear(iconStore).then(() =>
set("_version", iconMetadata.version, iconStore)
);
}
});
export const checkCacheVersion = async () => {
const version = await get("_version", iconStore);
if (!version) {
set("_version", iconMetadata.version, iconStore);
} else if (version !== iconMetadata.version) {
await clear(iconStore);
set("_version", iconMetadata.version, iconStore);
}
};

43
src/data/network.ts Normal file
View File

@@ -0,0 +1,43 @@
import { HomeAssistant } from "../types";
export interface IPv6ConfiguredAddress {
address: string;
flowinfo: number;
scope_id: number;
network_prefix: number;
}
export interface IPv4ConfiguredAddress {
address: string;
network_prefix: number;
}
export interface Adapter {
name: string;
enabled: boolean;
auto: boolean;
default: boolean;
ipv6: IPv6ConfiguredAddress[];
ipv4: IPv4ConfiguredAddress[];
}
export interface NetworkConfig {
adapters: Adapter[];
configured_adapters: string[];
}
export const getNetworkConfig = (hass: HomeAssistant) =>
hass.callWS<NetworkConfig>({
type: "network",
});
export const setNetworkConfig = (
hass: HomeAssistant,
configured_adapters: string[]
) =>
hass.callWS<string[]>({
type: "network/configure",
config: {
configured_adapters: configured_adapters,
},
});

View File

@@ -2,6 +2,7 @@ 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";
@@ -13,6 +14,28 @@ 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",

View File

@@ -1,14 +1,6 @@
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;

View File

@@ -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 {
getConfigEntrySystemOptions,
updateConfigEntrySystemOptions,
ConfigEntryMutableParams,
updateConfigEntry,
} 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,13 +35,8 @@ class DialogConfigEntrySystemOptions extends LitElement {
): Promise<void> {
this._params = params;
this._error = undefined;
this._loading = true;
const systemOptions = await getConfigEntrySystemOptions(
this.hass,
params.entry.entry_id
);
this._loading = false;
this._disableNewEntities = systemOptions.disable_new_entities;
this._disableNewEntities = params.entry.pref_disable_new_entities;
this._disablePolling = params.entry.pref_disable_polling;
}
public closeDialog(): void {
@@ -66,45 +61,57 @@ class DialogConfigEntrySystemOptions extends LitElement {
this._params.entry.domain
)}
>
<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>
${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>
`
: ""}
<mwc-button
slot="secondaryAction"
@click=${this.closeDialog}
@@ -115,7 +122,7 @@ class DialogConfigEntrySystemOptions extends LitElement {
<mwc-button
slot="primaryAction"
@click="${this._updateEntry}"
.disabled=${this._submitting || this._loading}
.disabled=${this._submitting}
>
${this.hass.localize("ui.dialogs.config_entry_system_options.update")}
</mwc-button>
@@ -123,22 +130,47 @@ 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 {
await updateConfigEntrySystemOptions(
const result = await updateConfigEntry(
this.hass,
this._params!.entry.entry_id,
{
disable_new_entities: this._disableNewEntities,
}
data
);
this._params = undefined;
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();
} catch (err) {
this._error = err.message || "Unknown error";
} finally {
@@ -150,20 +182,6 @@ 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);
}

View File

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

View File

@@ -108,12 +108,28 @@ export const showOptionsFlowDialog = (
`;
},
renderShowFormProgressHeader(_hass, _step) {
return "";
renderShowFormProgressHeader(hass, step) {
return (
hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.title`
) || hass.localize(`component.${configEntry.domain}.title`)
);
},
renderShowFormProgressDescription(_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>
`
: "";
},
}
);

View File

@@ -49,7 +49,7 @@ class DialogBox extends LitElement {
open
?scrimClickAction=${confirmPrompt}
?escapeKeyAction=${confirmPrompt}
@closing=${this._dialogClosed}
@closed=${this._dialogClosed}
defaultAction="ignore"
.heading=${this._params.title
? this._params.title

View File

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

View File

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

View File

@@ -23,16 +23,12 @@ 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>
`
: ""}
@@ -51,6 +47,11 @@ class MoreInfoPerson extends LitElement {
</div>
`
: ""}
<ha-attributes
.hass=${this.hass}
.stateObj=${this.stateObj}
extra-filters="id,user_id,editable"
></ha-attributes>
`;
}

View File

@@ -17,11 +17,6 @@ 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`
@@ -57,6 +52,11 @@ class MoreInfoTimer extends LitElement {
`
: ""}
</div>
<ha-attributes
.hass=${this.hass}
.stateObj=${this.stateObj}
extra-filters="remaining"
></ha-attributes>
`;
}

View File

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

View File

@@ -5,10 +5,11 @@ import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { throttle } from "../../common/util/throttle";
import "../../components/ha-circular-progress";
import "../../components/state-history-charts";
import { fetchUsers } from "../../data/user";
import { getLogbookData, LogbookEntry } from "../../data/logbook";
import { loadTraceContexts, TraceContexts } from "../../data/trace";
import "../../panels/logbook/ha-logbook";
import { haStyle, haStyleScrollbar } from "../../resources/styles";
import { haStyle } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { closeDialog } from "../make-dialog-manager";
@@ -22,10 +23,12 @@ export class MoreInfoLogbook extends LitElement {
@state() private _traceContexts?: TraceContexts;
@state() private _persons = {};
@state() private _userIdToName = {};
private _lastLogbookDate?: Date;
private _fetchUserPromise?: Promise<void>;
private _throttleGetLogbookEntries = throttle(() => {
this._getLogBookData();
}, 10000);
@@ -52,7 +55,6 @@ export class MoreInfoLogbook extends LitElement {
: this._logbookEntries.length
? html`
<ha-logbook
class="ha-scrollbar"
narrow
no-icon
no-name
@@ -60,7 +62,7 @@ export class MoreInfoLogbook extends LitElement {
.hass=${this.hass}
.entries=${this._logbookEntries}
.traceContexts=${this._traceContexts}
.userIdToName=${this._persons}
.userIdToName=${this._userIdToName}
></ha-logbook>
`
: html`<div class="no-entries">
@@ -71,7 +73,7 @@ export class MoreInfoLogbook extends LitElement {
}
protected firstUpdated(): void {
this._fetchPersonNames();
this._fetchUserPromise = this._fetchUserNames();
this.addEventListener("click", (ev) => {
if ((ev.composedPath()[0] as HTMLElement).tagName === "A") {
setTimeout(() => closeDialog("ha-more-info-dialog"), 500);
@@ -126,6 +128,7 @@ export class MoreInfoLogbook extends LitElement {
true
),
loadTraceContexts(this.hass),
this._fetchUserPromise,
]);
this._logbookEntries = this._logbookEntries
? [...newEntries, ...this._logbookEntries]
@@ -134,22 +137,39 @@ export class MoreInfoLogbook extends LitElement {
this._traceContexts = traceContexts;
}
private _fetchPersonNames() {
private async _fetchUserNames() {
const userIdToName = {};
// 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._persons[entity.attributes.user_id] =
this._userIdToName[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() {
return [
haStyle,
haStyleScrollbar,
css`
.no-entries {
text-align: center;
@@ -157,12 +177,11 @@ export class MoreInfoLogbook extends LitElement {
color: var(--secondary-text-color);
}
ha-logbook {
max-height: 250px;
overflow: auto;
--logbook-max-height: 250px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-logbook {
max-height: unset;
--logbook-max-height: unset;
}
}
ha-circular-progress {

View File

@@ -1,4 +1,4 @@
import { Layout1d, scroll } from "@lit-labs/virtualizer";
import { Layout1d, scroll } from "../../resources/lit-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,7 +188,6 @@ export class QuickBar extends LitElement {
${scroll({
items,
layout: Layout1d,
// @ts-expect-error
renderItem: (item: QuickBarItem, index) =>
this._renderItem(item, index),
})}
@@ -223,6 +222,9 @@ export class QuickBar extends LitElement {
}
private _renderItem(item: QuickBarItem, index?: number) {
if (!item) {
return html``;
}
return isCommandItem(item)
? this._renderCommandItem(item, index)
: this._renderEntityItem(item as EntityItem, index);
@@ -636,18 +638,6 @@ 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%;
}

View File

@@ -277,7 +277,7 @@ export const provideHass = (
mockTheme(theme) {
invalidateThemeCache();
hass().updateHass({
selectedThemeSettings: { theme: theme ? "mock" : "default" },
selectedTheme: { theme: theme ? "mock" : "default" },
themes: {
...hass().themes,
themes: {
@@ -285,11 +285,11 @@ export const provideHass = (
},
},
});
const { themes, selectedThemeSettings } = hass();
const { themes, selectedTheme } = hass();
applyThemesOnElement(
document.documentElement,
themes,
selectedThemeSettings!.theme
selectedTheme!.theme
);
},

View File

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

View File

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

View File

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

View File

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

View File

@@ -81,27 +81,25 @@ class SupervisorErrorScreen extends LitElement {
private _applyTheme() {
let themeName: string;
let themeSettings:
| Partial<HomeAssistant["selectedThemeSettings"]>
| undefined;
let themeSettings: Partial<HomeAssistant["selectedTheme"]> | undefined;
if (atLeastVersion(this.hass.config.version, 0, 114)) {
themeName =
this.hass.selectedThemeSettings?.theme ||
this.hass.selectedTheme?.theme ||
(this.hass.themes.darkMode && this.hass.themes.default_dark_theme
? this.hass.themes.default_dark_theme!
: this.hass.themes.default_theme);
themeSettings = this.hass.selectedThemeSettings;
themeSettings = this.hass.selectedTheme;
if (themeName === "default" && themeSettings?.dark === undefined) {
themeSettings = {
...this.hass.selectedThemeSettings,
...this.hass.selectedTheme,
dark: this.hass.themes.darkMode,
};
}
} else {
themeName =
((this.hass.selectedThemeSettings as unknown) as string) ||
((this.hass.selectedTheme as unknown) as string) ||
this.hass.themes.default_theme;
}

View File

@@ -12,7 +12,10 @@ 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 { fetchDiscoveryInformation } from "../data/discovery";
import {
DiscoveryInformation,
fetchDiscoveryInformation,
} from "../data/discovery";
import {
fetchOnboardingOverview,
OnboardingResponses,
@@ -68,6 +71,8 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
@state() private _steps?: OnboardingStep[];
@state() private _discoveryInformation?: DiscoveryInformation;
protected render(): TemplateResult {
const step = this._curStep()!;
@@ -87,6 +92,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
? html`<onboarding-restore-snapshot
.localize=${this.localize}
.restoring=${this._restoring}
.discoveryInformation=${this._discoveryInformation}
@restoring=${this._restoringSnapshot}
>
</onboarding-restore-snapshot>`

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