mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-16 00:19:40 +00:00
Compare commits
11 Commits
int2
...
20230110.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
24e6b8483e | ||
![]() |
604c452ff4 | ||
![]() |
ba9551b61e | ||
![]() |
135af5bcaa | ||
![]() |
747f47524e | ||
![]() |
36b959dbc4 | ||
![]() |
1d15f81b6c | ||
![]() |
caa852559f | ||
![]() |
ebb19e4ed5 | ||
![]() |
7cde3b66dd | ||
![]() |
2b8f7c46ff |
8
.github/workflows/cast_deployment.yaml
vendored
8
.github/workflows/cast_deployment.yaml
vendored
@@ -22,12 +22,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.3.0
|
||||
uses: actions/checkout@v3.2.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.6.0
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
@@ -60,12 +60,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.3.0
|
||||
uses: actions/checkout@v3.2.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.6.0
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
16
.github/workflows/ci.yaml
vendored
16
.github/workflows/ci.yaml
vendored
@@ -20,9 +20,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.3.0
|
||||
uses: actions/checkout@v3.2.0
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.6.0
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
@@ -44,9 +44,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.3.0
|
||||
uses: actions/checkout@v3.2.0
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.6.0
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
@@ -63,9 +63,9 @@ jobs:
|
||||
needs: [lint, test]
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.3.0
|
||||
uses: actions/checkout@v3.2.0
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.6.0
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
@@ -82,9 +82,9 @@ jobs:
|
||||
needs: [lint, test]
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.3.0
|
||||
uses: actions/checkout@v3.2.0
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.6.0
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.3.0
|
||||
uses: actions/checkout@v3.2.0
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
8
.github/workflows/demo_deployment.yaml
vendored
8
.github/workflows/demo_deployment.yaml
vendored
@@ -23,12 +23,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.3.0
|
||||
uses: actions/checkout@v3.2.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.6.0
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
@@ -61,12 +61,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.3.0
|
||||
uses: actions/checkout@v3.2.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.6.0
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
4
.github/workflows/design_deployment.yaml
vendored
4
.github/workflows/design_deployment.yaml
vendored
@@ -17,10 +17,10 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.3.0
|
||||
uses: actions/checkout@v3.2.0
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.6.0
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
4
.github/workflows/design_preview.yaml
vendored
4
.github/workflows/design_preview.yaml
vendored
@@ -22,10 +22,10 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.3.0
|
||||
uses: actions/checkout@v3.2.0
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.6.0
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
4
.github/workflows/nightly.yaml
vendored
4
.github/workflows/nightly.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.3.0
|
||||
uses: actions/checkout@v3.2.0
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v4
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.6.0
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.3.0
|
||||
uses: actions/checkout@v3.2.0
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.6.0
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.3.0
|
||||
uses: actions/checkout@v3.2.0
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -8,7 +8,7 @@ dist/
|
||||
/translations/
|
||||
|
||||
# yarn
|
||||
.yarn/*
|
||||
.yarn/**
|
||||
!.yarn/patches
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
@@ -31,7 +31,7 @@ pip-selfcheck.json
|
||||
.venv
|
||||
|
||||
# vscode
|
||||
.vscode/*
|
||||
.vscode/**
|
||||
!.vscode/extensions.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/tasks.json
|
||||
|
12
.yarn/patches/@material/mwc-icon-button/remove-icon.patch
Normal file
12
.yarn/patches/@material/mwc-icon-button/remove-icon.patch
Normal file
@@ -0,0 +1,12 @@
|
||||
diff --git a/mwc-icon-button-base.js b/mwc-icon-button-base.js
|
||||
index 45cdaab93ccc0a6daaaaabc01266dcdc32e46bfd..b3ea5b541597308d85f86ce6c23fd00785fda835 100644
|
||||
--- a/mwc-icon-button-base.js
|
||||
+++ b/mwc-icon-button-base.js
|
||||
@@ -63,7 +63,6 @@ export class IconButtonBase extends LitElement {
|
||||
@touchend="${this.handleRippleDeactivate}"
|
||||
@touchcancel="${this.handleRippleDeactivate}"
|
||||
>${this.renderRipple()}
|
||||
- <i class="material-icons">${this.icon}</i>
|
||||
<span
|
||||
><slot></slot
|
||||
></span>
|
@@ -1,40 +1,36 @@
|
||||
const del = import("del");
|
||||
const del = require("del");
|
||||
const gulp = require("gulp");
|
||||
const paths = require("../paths");
|
||||
require("./translations");
|
||||
|
||||
gulp.task(
|
||||
"clean",
|
||||
gulp.parallel("clean-translations", async () =>
|
||||
(await del).deleteSync([paths.app_output_root, paths.build_dir])
|
||||
gulp.parallel("clean-translations", () =>
|
||||
del([paths.app_output_root, paths.build_dir])
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"clean-demo",
|
||||
gulp.parallel("clean-translations", async () =>
|
||||
(await del).deleteSync([paths.demo_output_root, paths.build_dir])
|
||||
gulp.parallel("clean-translations", () =>
|
||||
del([paths.demo_output_root, paths.build_dir])
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"clean-cast",
|
||||
gulp.parallel("clean-translations", async () =>
|
||||
(await del).deleteSync([paths.cast_output_root, paths.build_dir])
|
||||
gulp.parallel("clean-translations", () =>
|
||||
del([paths.cast_output_root, paths.build_dir])
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("clean-hassio", async () =>
|
||||
(await del).deleteSync([paths.hassio_output_root, paths.build_dir])
|
||||
gulp.task("clean-hassio", () =>
|
||||
del([paths.hassio_output_root, paths.build_dir])
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"clean-gallery",
|
||||
gulp.parallel("clean-translations", async () =>
|
||||
(await del).deleteSync([
|
||||
paths.gallery_output_root,
|
||||
paths.gallery_build,
|
||||
paths.build_dir,
|
||||
])
|
||||
gulp.parallel("clean-translations", () =>
|
||||
del([paths.gallery_output_root, paths.gallery_build, paths.build_dir])
|
||||
)
|
||||
);
|
||||
|
@@ -1,9 +1,9 @@
|
||||
// Task to download the latest Lokalise translations from the nightly workflow artifacts
|
||||
|
||||
const del = import("del");
|
||||
const fs = require("fs/promises");
|
||||
const path = require("path");
|
||||
const process = require("process");
|
||||
const del = require("del");
|
||||
const gulp = require("gulp");
|
||||
const jszip = require("jszip");
|
||||
const tar = require("tar");
|
||||
@@ -17,8 +17,8 @@ const WORKFLOW_NAME = "nightly.yaml";
|
||||
const ARTIFACT_NAME = "translations";
|
||||
const CLIENT_ID = "Iv1.3914e28cb27834d1";
|
||||
const EXTRACT_DIR = "translations";
|
||||
const TOKEN_FILE = path.posix.join(EXTRACT_DIR, "token.json");
|
||||
const ARTIFACT_FILE = path.posix.join(EXTRACT_DIR, "artifact.json");
|
||||
const TOKEN_FILE = path.join(EXTRACT_DIR, "token.json");
|
||||
const ARTIFACT_FILE = path.join(EXTRACT_DIR, "artifact.json");
|
||||
|
||||
let allowTokenSetup = false;
|
||||
gulp.task("allow-setup-fetch-nightly-translations", (done) => {
|
||||
@@ -137,11 +137,7 @@ gulp.task("fetch-nightly-translations", async function () {
|
||||
|
||||
// Remove the current translations
|
||||
const deleteCurrent = Promise.all(writings).then(
|
||||
(await del).deleteAsync([
|
||||
`${EXTRACT_DIR}/*`,
|
||||
`!${ARTIFACT_FILE}`,
|
||||
`!${TOKEN_FILE}`,
|
||||
])
|
||||
del([`${EXTRACT_DIR}/*`, `!${ARTIFACT_FILE}`, `!${TOKEN_FILE}`])
|
||||
);
|
||||
|
||||
// Get the download URL and follow the redirect to download (stored as ArrayBuffer)
|
@@ -1,4 +1,4 @@
|
||||
const del = import("del");
|
||||
const del = require("del");
|
||||
const path = require("path");
|
||||
const gulp = require("gulp");
|
||||
const fs = require("fs");
|
||||
@@ -6,7 +6,7 @@ const paths = require("../paths");
|
||||
|
||||
const outDir = "build/locale-data";
|
||||
|
||||
gulp.task("clean-locale-data", async () => (await del).deleteSync([outDir]));
|
||||
gulp.task("clean-locale-data", () => del([outDir]));
|
||||
|
||||
gulp.task("ensure-locale-data-build-dir", (done) => {
|
||||
if (!fs.existsSync(outDir)) {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
const del = import("del");
|
||||
const crypto = require("crypto");
|
||||
const del = require("del");
|
||||
const path = require("path");
|
||||
const source = require("vinyl-source-stream");
|
||||
const vinylBuffer = require("vinyl-buffer");
|
||||
@@ -13,7 +13,7 @@ const { mapFiles } = require("../util");
|
||||
const env = require("../env");
|
||||
const paths = require("../paths");
|
||||
|
||||
require("./fetch-nightly-translations");
|
||||
require("./fetch-nightly_translations");
|
||||
|
||||
const inFrontendDir = "translations/frontend";
|
||||
const inBackendDir = "translations/backend";
|
||||
@@ -120,7 +120,7 @@ function lokaliseTransform(data, original, file) {
|
||||
return output;
|
||||
}
|
||||
|
||||
gulp.task("clean-translations", async () => (await del).deleteSync([workDir]));
|
||||
gulp.task("clean-translations", () => del([workDir]));
|
||||
|
||||
gulp.task("ensure-translations-build-dir", (done) => {
|
||||
if (!fs.existsSync(workDir)) {
|
||||
|
130
package.json
130
package.json
@@ -25,16 +25,21 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^6.0.0",
|
||||
"@codemirror/autocomplete": "^6.4.0",
|
||||
"@codemirror/commands": "^6.1.3",
|
||||
"@codemirror/language": "^6.3.2",
|
||||
"@codemirror/legacy-modes": "^6.3.1",
|
||||
"@codemirror/search": "^6.2.3",
|
||||
"@codemirror/state": "^6.2.0",
|
||||
"@codemirror/view": "^6.7.1",
|
||||
"@codemirror/autocomplete": "^0.19.12",
|
||||
"@codemirror/commands": "^0.19.8",
|
||||
"@codemirror/gutter": "^0.19.9",
|
||||
"@codemirror/highlight": "^0.19.7",
|
||||
"@codemirror/history": "^0.19.2",
|
||||
"@codemirror/legacy-modes": "^0.19.0",
|
||||
"@codemirror/rectangular-selection": "^0.19.1",
|
||||
"@codemirror/search": "^0.19.6",
|
||||
"@codemirror/state": "^0.19.6",
|
||||
"@codemirror/stream-parser": "^0.19.5",
|
||||
"@codemirror/text": "^0.19.6",
|
||||
"@codemirror/view": "^0.19.40",
|
||||
"@formatjs/intl-datetimeformat": "^4.2.5",
|
||||
"@formatjs/intl-getcanonicallocales": "^1.8.0",
|
||||
"@formatjs/intl-locale": "^3.0.11",
|
||||
"@formatjs/intl-locale": "^2.4.40",
|
||||
"@formatjs/intl-numberformat": "^7.2.5",
|
||||
"@formatjs/intl-pluralrules": "^4.1.5",
|
||||
"@formatjs/intl-relativetimeformat": "^9.3.2",
|
||||
@@ -44,35 +49,34 @@
|
||||
"@fullcalendar/interaction": "5.9.0",
|
||||
"@fullcalendar/list": "5.9.0",
|
||||
"@fullcalendar/timegrid": "5.9.0",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@lit-labs/motion": "^1.0.2",
|
||||
"@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.7.0-pre.2#./.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch",
|
||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/mwc-button": "^0.27.0",
|
||||
"@material/mwc-checkbox": "^0.27.0",
|
||||
"@material/mwc-circular-progress": "^0.27.0",
|
||||
"@material/mwc-dialog": "^0.27.0",
|
||||
"@material/mwc-drawer": "^0.27.0",
|
||||
"@material/mwc-fab": "^0.27.0",
|
||||
"@material/mwc-formfield": "^0.27.0",
|
||||
"@material/mwc-icon-button": "^0.27.0",
|
||||
"@material/mwc-linear-progress": "^0.27.0",
|
||||
"@material/mwc-list": "^0.27.0",
|
||||
"@material/mwc-menu": "^0.27.0",
|
||||
"@material/mwc-radio": "^0.27.0",
|
||||
"@material/mwc-ripple": "^0.27.0",
|
||||
"@material/mwc-select": "^0.27.0",
|
||||
"@material/mwc-slider": "^0.27.0",
|
||||
"@material/mwc-switch": "^0.27.0",
|
||||
"@material/mwc-tab": "^0.27.0",
|
||||
"@material/mwc-tab-bar": "^0.27.0",
|
||||
"@material/mwc-textarea": "^0.27.0",
|
||||
"@material/mwc-textfield": "^0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "^0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@mdi/js": "7.1.96",
|
||||
"@mdi/svg": "7.1.96",
|
||||
"@material/chips": "14.0.0-canary.261f2db59.0",
|
||||
"@material/data-table": "14.0.0-canary.261f2db59.0",
|
||||
"@material/mwc-button": "0.25.3",
|
||||
"@material/mwc-checkbox": "0.25.3",
|
||||
"@material/mwc-circular-progress": "0.25.3",
|
||||
"@material/mwc-dialog": "0.25.3",
|
||||
"@material/mwc-drawer": "^0.25.3",
|
||||
"@material/mwc-fab": "0.25.3",
|
||||
"@material/mwc-formfield": "0.25.3",
|
||||
"@material/mwc-icon-button": "patch:@material/mwc-icon-button@0.25.3#./.yarn/patches/@material/mwc-icon-button/remove-icon.patch",
|
||||
"@material/mwc-linear-progress": "0.25.3",
|
||||
"@material/mwc-list": "^0.25.3",
|
||||
"@material/mwc-menu": "0.25.3",
|
||||
"@material/mwc-radio": "0.25.3",
|
||||
"@material/mwc-ripple": "0.25.3",
|
||||
"@material/mwc-select": "0.25.3",
|
||||
"@material/mwc-slider": "0.25.3",
|
||||
"@material/mwc-switch": "0.25.3",
|
||||
"@material/mwc-tab": "0.25.3",
|
||||
"@material/mwc-tab-bar": "0.25.3",
|
||||
"@material/mwc-textarea": "^0.25.3",
|
||||
"@material/mwc-textfield": "0.25.3",
|
||||
"@material/mwc-top-app-bar-fixed": "^0.25.3",
|
||||
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
|
||||
"@mdi/js": "7.0.96",
|
||||
"@mdi/svg": "7.0.96",
|
||||
"@polymer/app-layout": "^3.1.0",
|
||||
"@polymer/iron-flex-layout": "^3.0.1",
|
||||
"@polymer/iron-icon": "^3.0.1",
|
||||
@@ -87,13 +91,13 @@
|
||||
"@polymer/paper-toast": "^3.0.1",
|
||||
"@polymer/paper-tooltip": "^3.0.1",
|
||||
"@polymer/polymer": "3.4.1",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@thomasloven/round-slider": "0.5.4",
|
||||
"@vaadin/combo-box": "^23.2.9",
|
||||
"@vaadin/vaadin-themable-mixin": "^23.2.9",
|
||||
"@vibrant/color": "^3.2.1-alpha.1",
|
||||
"@vibrant/core": "^3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
|
||||
"@vue/web-component-wrapper": "^1.3.0",
|
||||
"@vue/web-component-wrapper": "^1.2.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "^0.0.5",
|
||||
"@webcomponents/webcomponentsjs": "^2.2.10",
|
||||
"app-datepicker": "^5.1.0",
|
||||
@@ -108,7 +112,7 @@
|
||||
"fuse.js": "^6.0.0",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hammerjs": "^2.0.8",
|
||||
"hls.js": "^1.3.1",
|
||||
"hls.js": "^1.2.5",
|
||||
"home-assistant-js-websocket": "^8.0.1",
|
||||
"idb-keyval": "^5.1.3",
|
||||
"intl-messageformat": "^9.9.1",
|
||||
@@ -117,13 +121,13 @@
|
||||
"leaflet-draw": "^1.0.4",
|
||||
"lit": "^2.1.2",
|
||||
"marked": "^4.0.12",
|
||||
"memoize-one": "^6.0.0",
|
||||
"memoize-one": "^5.2.1",
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "^0.3.2",
|
||||
"punycode": "^2.1.1",
|
||||
"qr-scanner": "^1.3.0",
|
||||
"qrcode": "^1.4.4",
|
||||
"regenerator-runtime": "^0.13.11",
|
||||
"regenerator-runtime": "^0.13.8",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"rrule": "^2.7.1",
|
||||
@@ -137,19 +141,19 @@
|
||||
"vue": "^2.6.12",
|
||||
"vue2-daterange-picker": "^0.5.1",
|
||||
"weekstart": "^1.1.0",
|
||||
"workbox-cacheable-response": "^6.5.4",
|
||||
"workbox-core": "^6.5.4",
|
||||
"workbox-expiration": "^6.5.4",
|
||||
"workbox-precaching": "^6.5.4",
|
||||
"workbox-routing": "^6.5.4",
|
||||
"workbox-strategies": "^6.5.4",
|
||||
"xss": "^1.0.14"
|
||||
"workbox-cacheable-response": "^6.4.2",
|
||||
"workbox-core": "^6.4.2",
|
||||
"workbox-expiration": "^6.4.2",
|
||||
"workbox-precaching": "^6.4.2",
|
||||
"workbox-routing": "^6.4.2",
|
||||
"workbox-strategies": "^6.4.2",
|
||||
"xss": "^1.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.2",
|
||||
"@babel/plugin-external-helpers": "^7.18.6",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-proposal-decorators": "^7.20.7",
|
||||
"@babel/plugin-proposal-decorators": "^7.20.2",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.20.2",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.18.9",
|
||||
@@ -160,7 +164,7 @@
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@koa/cors": "^3.1.0",
|
||||
"@octokit/auth-oauth-device": "^4.0.2",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
"@octokit/rest": "^19.0.4",
|
||||
"@open-wc/dev-server-hmr": "^0.0.2",
|
||||
"@rollup/plugin-babel": "^5.2.1",
|
||||
"@rollup/plugin-commonjs": "^11.1.0",
|
||||
@@ -169,7 +173,7 @@
|
||||
"@rollup/plugin-replace": "^2.3.2",
|
||||
"@types/chromecast-caf-receiver": "5.0.12",
|
||||
"@types/chromecast-caf-sender": "^1.0.3",
|
||||
"@types/glob": "^8",
|
||||
"@types/glob": "^7",
|
||||
"@types/hammerjs": "^2.0.41",
|
||||
"@types/js-yaml": "^4",
|
||||
"@types/leaflet": "^1",
|
||||
@@ -186,7 +190,7 @@
|
||||
"@web/dev-server-rollup": "^0.2.11",
|
||||
"babel-loader": "^9.1.0",
|
||||
"chai": "^4.3.4",
|
||||
"del": "^7.0.0",
|
||||
"del": "^4.0.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-config-airbnb-typescript": "^14.0.0",
|
||||
@@ -196,10 +200,10 @@
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"eslint-plugin-lit": "^1.6.1",
|
||||
"eslint-plugin-unused-imports": "^1.1.5",
|
||||
"eslint-plugin-wc": "^1.4.0",
|
||||
"eslint-plugin-wc": "^1.3.2",
|
||||
"fancy-log": "^2.0.0",
|
||||
"fs-extra": "^11.1.0",
|
||||
"glob": "^8.1.0",
|
||||
"fs-extra": "^7.0.1",
|
||||
"glob": "^7.2.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-flatmap": "^1.0.2",
|
||||
"gulp-json-transform": "^0.4.6",
|
||||
@@ -210,38 +214,38 @@
|
||||
"husky": "^8.0.1",
|
||||
"instant-mocha": "^1.3.1",
|
||||
"jszip": "^3.10.1",
|
||||
"lint-staged": "^13.1.0",
|
||||
"lint-staged": "^13.0.3",
|
||||
"lit-analyzer": "^1.2.1",
|
||||
"lodash.template": "^4.5.0",
|
||||
"magic-string": "^0.25.7",
|
||||
"map-stream": "^0.0.7",
|
||||
"merge-stream": "^1.0.1",
|
||||
"mocha": "^8.4.0",
|
||||
"object-hash": "^3.0.0",
|
||||
"open": "^8.4.0",
|
||||
"object-hash": "^2.0.3",
|
||||
"open": "^7.0.4",
|
||||
"pinst": "^3.0.0",
|
||||
"prettier": "^2.8.3",
|
||||
"prettier": "^2.8.1",
|
||||
"require-dir": "^1.2.0",
|
||||
"rollup": "^2.8.2",
|
||||
"rollup-plugin-string": "^3.0.0",
|
||||
"rollup-plugin-terser": "^5.3.0",
|
||||
"rollup-plugin-visualizer": "^5.9.0",
|
||||
"rollup-plugin-visualizer": "^4.0.4",
|
||||
"serve": "^11.3.2",
|
||||
"sinon": "^15.0.1",
|
||||
"sinon": "^11.0.0",
|
||||
"source-map-url": "^0.4.0",
|
||||
"systemjs": "^6.3.2",
|
||||
"tar": "^6.1.11",
|
||||
"terser-webpack-plugin": "^5.2.4",
|
||||
"ts-lit-plugin": "^1.2.1",
|
||||
"typescript": "^4.9.4",
|
||||
"typescript": "^4.9.3",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"vinyl-source-stream": "^2.0.0",
|
||||
"webpack": "^5.55.1",
|
||||
"webpack-cli": "^4.8.0",
|
||||
"webpack-dev-server": "^4.11.1",
|
||||
"webpack-dev-server": "^4.3.0",
|
||||
"webpack-manifest-plugin": "^4.0.2",
|
||||
"webpackbar": "^5.0.0-3",
|
||||
"workbox-build": "^6.5.4"
|
||||
"workbox-build": "^6.4.2"
|
||||
},
|
||||
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
|
||||
"resolutions": {
|
||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20230104.0"
|
||||
version = "20230110.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@@ -201,7 +201,6 @@ export const DOMAINS_WITH_CARD = [
|
||||
export const SENSOR_ENTITIES = [
|
||||
"sensor",
|
||||
"binary_sensor",
|
||||
"calendar",
|
||||
"camera",
|
||||
"device_tracker",
|
||||
"weather",
|
||||
|
@@ -39,5 +39,5 @@ export default function scrollToTarget(element, target) {
|
||||
);
|
||||
requestAnimationFrame(updateFrame.bind(element));
|
||||
}
|
||||
}).call(element);
|
||||
}.call(element));
|
||||
}
|
||||
|
@@ -108,7 +108,7 @@ export const domainIconWithoutDefault = (
|
||||
return compareState === "not_home" ? mdiAccountArrowRight : mdiAccount;
|
||||
|
||||
case "humidifier":
|
||||
return compareState === "off" ? mdiAirHumidifierOff : mdiAirHumidifier;
|
||||
return state && state === "off" ? mdiAirHumidifierOff : mdiAirHumidifier;
|
||||
|
||||
case "input_boolean":
|
||||
return compareState === "on"
|
||||
|
@@ -23,9 +23,7 @@ const STATIC_ACTIVE_COLORED_DOMAIN = new Set([
|
||||
"input_boolean",
|
||||
"light",
|
||||
"media_player",
|
||||
"plant",
|
||||
"remote",
|
||||
"schedule",
|
||||
"script",
|
||||
"siren",
|
||||
"switch",
|
||||
|
@@ -5,6 +5,7 @@ import DateRangePicker from "vue2-daterange-picker";
|
||||
// @ts-ignore
|
||||
import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { Constructor } from "../types";
|
||||
|
||||
const Component = Vue.extend({
|
||||
props: {
|
||||
@@ -46,26 +47,35 @@ const Component = Vue.extend({
|
||||
},
|
||||
},
|
||||
render(createElement) {
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
return createElement(DateRangePicker, {
|
||||
props: {
|
||||
// @ts-ignore
|
||||
"time-picker": this.timePicker,
|
||||
// @ts-ignore
|
||||
"auto-apply": this.autoApply,
|
||||
opens: "right",
|
||||
"show-dropdowns": false,
|
||||
// @ts-ignore
|
||||
"time-picker24-hour": this.twentyfourHours,
|
||||
// @ts-ignore
|
||||
disabled: this.disabled,
|
||||
// @ts-ignore
|
||||
ranges: this.ranges ? {} : false,
|
||||
"locale-data": {
|
||||
// @ts-ignore
|
||||
firstDay: this.firstDay,
|
||||
},
|
||||
},
|
||||
model: {
|
||||
value: {
|
||||
// @ts-ignore
|
||||
startDate: this.startDate,
|
||||
// @ts-ignore
|
||||
endDate: this.endDate,
|
||||
},
|
||||
callback: (value) => {
|
||||
// @ts-ignore
|
||||
fireEvent(this.$el as HTMLElement, "change", value);
|
||||
},
|
||||
expression: "dateRange",
|
||||
@@ -96,11 +106,7 @@ const Component = Vue.extend({
|
||||
},
|
||||
});
|
||||
|
||||
// Assertion corrects HTMLElement type from package
|
||||
const WrappedElement = wrap(
|
||||
Vue,
|
||||
Component
|
||||
) as unknown as CustomElementConstructor;
|
||||
const WrappedElement: Constructor<HTMLElement> = wrap(Vue, Component);
|
||||
|
||||
@customElement("date-range-picker")
|
||||
class DateRangePickerElement extends WrappedElement {
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||
import type { PolymerChangedEvent } from "../../polymer-types";
|
||||
@@ -96,10 +95,7 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
.excludeEntities=${this.excludeEntities}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||
.entityFilter=${this._getEntityFilter(
|
||||
this.value,
|
||||
this.entityFilter
|
||||
)}
|
||||
.entityFilter=${this._entityFilter}
|
||||
.value=${entityId}
|
||||
.label=${this.pickedEntityLabel}
|
||||
.disabled=${this.disabled}
|
||||
@@ -118,7 +114,7 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
.excludeEntities=${this.excludeEntities}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||
.entityFilter=${this._getEntityFilter(this.value, this.entityFilter)}
|
||||
.entityFilter=${this._entityFilter}
|
||||
.label=${this.pickEntityLabel}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
@@ -129,15 +125,11 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _getEntityFilter = memoizeOne(
|
||||
(
|
||||
value: string[] | undefined,
|
||||
entityFilter: HaEntityPickerEntityFilterFunc | undefined
|
||||
): HaEntityPickerEntityFilterFunc =>
|
||||
(stateObj: HassEntity) =>
|
||||
(!value || !value.includes(stateObj.entity_id)) &&
|
||||
(!entityFilter || entityFilter(stateObj))
|
||||
);
|
||||
private _entityFilter: HaEntityPickerEntityFilterFunc = (
|
||||
stateObj: HassEntity
|
||||
) =>
|
||||
(!this.value || !this.value.includes(stateObj.entity_id)) &&
|
||||
(!this.entityFilter || this.entityFilter(stateObj));
|
||||
|
||||
private get _currentEntities() {
|
||||
return this.value || [];
|
||||
|
@@ -35,9 +35,9 @@ const TRUNCATED_DOMAINS = [
|
||||
"person",
|
||||
] as const satisfies ReadonlyArray<keyof typeof FIXED_DOMAIN_STATES>;
|
||||
|
||||
type TruncatedDomain = (typeof TRUNCATED_DOMAINS)[number];
|
||||
type TruncatedDomain = typeof TRUNCATED_DOMAINS[number];
|
||||
type TruncatedKey = {
|
||||
[T in TruncatedDomain]: `${T}.${(typeof FIXED_DOMAIN_STATES)[T][number]}`;
|
||||
[T in TruncatedDomain]: `${T}.${typeof FIXED_DOMAIN_STATES[T][number]}`;
|
||||
}[TruncatedDomain];
|
||||
|
||||
const getTruncatedKey = (domainKey: string, stateKey: string) => {
|
||||
|
@@ -278,11 +278,6 @@ export class HaBarSlider extends LitElement {
|
||||
--slider-bar-border-radius: 10px;
|
||||
height: var(--slider-bar-thickness);
|
||||
width: 100%;
|
||||
border-radius: var(--slider-bar-border-radius);
|
||||
outline: none;
|
||||
}
|
||||
:host(:focus-visible) {
|
||||
box-shadow: 0 0 0 2px var(--slider-bar-color);
|
||||
}
|
||||
:host([vertical]) {
|
||||
width: var(--slider-bar-thickness);
|
||||
|
@@ -104,14 +104,6 @@ export class HaBarSwitch extends LitElement {
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
border-radius: var(--switch-bar-border-radius);
|
||||
outline: none;
|
||||
}
|
||||
:host(:focus-visible) {
|
||||
box-shadow: 0 0 0 2px var(--switch-bar-off-color);
|
||||
}
|
||||
:host([checked]:focus-visible) {
|
||||
box-shadow: 0 0 0 2px var(--switch-bar-on-color);
|
||||
}
|
||||
.switch {
|
||||
box-sizing: border-box;
|
||||
|
@@ -4,7 +4,6 @@ import type {
|
||||
CompletionResult,
|
||||
CompletionSource,
|
||||
} from "@codemirror/autocomplete";
|
||||
import type { Extension } from "@codemirror/state";
|
||||
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
|
||||
import { HassEntities } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
|
||||
@@ -73,9 +72,9 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
if (!this.codemirror || !this._loadedCodeMirror) {
|
||||
return false;
|
||||
}
|
||||
const className = this._loadedCodeMirror.highlightingFor(
|
||||
const className = this._loadedCodeMirror.HighlightStyle.get(
|
||||
this.codemirror.state,
|
||||
[this._loadedCodeMirror.tags.comment]
|
||||
this._loadedCodeMirror.tags.comment
|
||||
);
|
||||
return !!this.shadowRoot!.querySelector(`span.${className}`);
|
||||
}
|
||||
@@ -137,7 +136,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
|
||||
private async _load(): Promise<void> {
|
||||
this._loadedCodeMirror = await loadCodeMirror();
|
||||
const extensions: Extension[] = [
|
||||
const extensions = [
|
||||
this._loadedCodeMirror.lineNumbers(),
|
||||
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
|
||||
this._loadedCodeMirror.history(),
|
||||
@@ -153,8 +152,10 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
saveKeyBinding,
|
||||
] as KeyBinding[]),
|
||||
this._loadedCodeMirror.langCompartment.of(this._mode),
|
||||
this._loadedCodeMirror.haTheme,
|
||||
this._loadedCodeMirror.haSyntaxHighlighting,
|
||||
this._loadedCodeMirror.theme,
|
||||
this._loadedCodeMirror.Prec.fallback(
|
||||
this._loadedCodeMirror.highlightStyle
|
||||
),
|
||||
this._loadedCodeMirror.readonlyCompartment.of(
|
||||
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
|
||||
),
|
||||
@@ -226,7 +227,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
return {
|
||||
from: Number(entityWord.from),
|
||||
options: states,
|
||||
validFor: /^[a-z_]{3,}\.\w*$/,
|
||||
span: /^[a-z_]{3,}\.\w*$/,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -267,7 +268,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
return {
|
||||
from: Number(match.from),
|
||||
options: iconItems,
|
||||
validFor: /^mdi:\S*$/,
|
||||
span: /^mdi:\S*$/,
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -164,11 +164,10 @@ export class HaSelectSelector extends LitElement {
|
||||
<ha-select
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.label=${this.label ?? ""}
|
||||
.value=${this.value ?? ""}
|
||||
.helper=${this.helper ?? ""}
|
||||
.label=${this.label}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
@closed=${stopPropagation}
|
||||
@selected=${this._valueChanged}
|
||||
>
|
||||
|
@@ -8,7 +8,7 @@ import { BlueprintInput } from "./blueprint";
|
||||
import { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
import { Action, MODES } from "./script";
|
||||
|
||||
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
|
||||
export const AUTOMATION_DEFAULT_MODE: typeof MODES[number] = "single";
|
||||
export const AUTOMATION_DEFAULT_MAX = 10;
|
||||
|
||||
export interface AutomationEntity extends HassEntityBase {
|
||||
@@ -29,7 +29,7 @@ export interface ManualAutomationConfig {
|
||||
trigger: Trigger | Trigger[];
|
||||
condition?: Condition | Condition[];
|
||||
action: Action | Action[];
|
||||
mode?: (typeof MODES)[number];
|
||||
mode?: typeof MODES[number];
|
||||
max?: number;
|
||||
max_exceeded?:
|
||||
| "silent"
|
||||
|
200
src/data/cached-history.ts
Normal file
200
src/data/cached-history.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import { HomeAssistant } from "../types";
|
||||
import {
|
||||
computeHistory,
|
||||
HistoryStates,
|
||||
HistoryResult,
|
||||
LineChartUnit,
|
||||
TimelineEntity,
|
||||
entityIdHistoryNeedsAttributes,
|
||||
fetchRecentWS,
|
||||
} from "./history";
|
||||
|
||||
export interface CacheConfig {
|
||||
cacheKey: string;
|
||||
hoursToShow: number;
|
||||
}
|
||||
|
||||
interface CachedResults {
|
||||
prom: Promise<HistoryResult>;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
language: string;
|
||||
data: HistoryResult;
|
||||
}
|
||||
|
||||
const stateHistoryCache: { [cacheKey: string]: CachedResults } = {};
|
||||
|
||||
// Cache type 2 functionality
|
||||
function getEmptyCache(
|
||||
language: string,
|
||||
startTime: Date,
|
||||
endTime: Date
|
||||
): CachedResults {
|
||||
return {
|
||||
prom: Promise.resolve({ line: [], timeline: [] }),
|
||||
language,
|
||||
startTime,
|
||||
endTime,
|
||||
data: { line: [], timeline: [] },
|
||||
};
|
||||
}
|
||||
|
||||
export const getRecentWithCache = (
|
||||
hass: HomeAssistant,
|
||||
entityIds: string[],
|
||||
cacheConfig: CacheConfig,
|
||||
localize: LocalizeFunc,
|
||||
language: string
|
||||
) => {
|
||||
const cacheKey = cacheConfig.cacheKey;
|
||||
const fullCacheKey = cacheKey + `_${cacheConfig.hoursToShow}`;
|
||||
const endTime = new Date();
|
||||
const startTime = new Date(endTime);
|
||||
startTime.setHours(startTime.getHours() - cacheConfig.hoursToShow);
|
||||
let toFetchStartTime = startTime;
|
||||
let appendingToCache = false;
|
||||
|
||||
let cache = stateHistoryCache[fullCacheKey];
|
||||
if (
|
||||
cache &&
|
||||
toFetchStartTime >= cache.startTime &&
|
||||
toFetchStartTime <= cache.endTime &&
|
||||
cache.language === language
|
||||
) {
|
||||
toFetchStartTime = cache.endTime;
|
||||
appendingToCache = true;
|
||||
// This pretty much never happens as endTime is usually set to now
|
||||
if (endTime <= cache.endTime) {
|
||||
return cache.prom;
|
||||
}
|
||||
} else {
|
||||
cache = stateHistoryCache[fullCacheKey] = getEmptyCache(
|
||||
language,
|
||||
startTime,
|
||||
endTime
|
||||
);
|
||||
}
|
||||
|
||||
const curCacheProm = cache.prom;
|
||||
const noAttributes = !entityIds.some((entityId) =>
|
||||
entityIdHistoryNeedsAttributes(hass, entityId)
|
||||
);
|
||||
|
||||
const genProm = async () => {
|
||||
let fetchedHistory: HistoryStates;
|
||||
|
||||
try {
|
||||
const results = await Promise.all([
|
||||
curCacheProm,
|
||||
fetchRecentWS(
|
||||
hass,
|
||||
entityIds,
|
||||
toFetchStartTime,
|
||||
endTime,
|
||||
appendingToCache,
|
||||
undefined,
|
||||
true,
|
||||
noAttributes
|
||||
),
|
||||
]);
|
||||
fetchedHistory = results[1];
|
||||
} catch (err: any) {
|
||||
delete stateHistoryCache[fullCacheKey];
|
||||
throw err;
|
||||
}
|
||||
const stateHistory = computeHistory(hass, fetchedHistory, localize);
|
||||
if (appendingToCache) {
|
||||
if (stateHistory.line.length) {
|
||||
mergeLine(stateHistory.line, cache.data.line);
|
||||
}
|
||||
if (stateHistory.timeline.length) {
|
||||
mergeTimeline(stateHistory.timeline, cache.data.timeline);
|
||||
// Replace the timeline array to force an update
|
||||
cache.data.timeline = [...cache.data.timeline];
|
||||
}
|
||||
pruneStartTime(startTime, cache.data);
|
||||
} else {
|
||||
cache.data = stateHistory;
|
||||
}
|
||||
return cache.data;
|
||||
};
|
||||
|
||||
cache.prom = genProm();
|
||||
cache.startTime = startTime;
|
||||
cache.endTime = endTime;
|
||||
return cache.prom;
|
||||
};
|
||||
|
||||
const mergeLine = (
|
||||
historyLines: LineChartUnit[],
|
||||
cacheLines: LineChartUnit[]
|
||||
) => {
|
||||
historyLines.forEach((line) => {
|
||||
const unit = line.unit;
|
||||
const oldLine = cacheLines.find((cacheLine) => cacheLine.unit === unit);
|
||||
if (oldLine) {
|
||||
line.data.forEach((entity) => {
|
||||
const oldEntity = oldLine.data.find(
|
||||
(cacheEntity) => entity.entity_id === cacheEntity.entity_id
|
||||
);
|
||||
if (oldEntity) {
|
||||
oldEntity.states = oldEntity.states.concat(entity.states);
|
||||
} else {
|
||||
oldLine.data.push(entity);
|
||||
}
|
||||
});
|
||||
// Replace the cached line data to force an update
|
||||
oldLine.data = [...oldLine.data];
|
||||
} else {
|
||||
cacheLines.push(line);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const mergeTimeline = (
|
||||
historyTimelines: TimelineEntity[],
|
||||
cacheTimelines: TimelineEntity[]
|
||||
) => {
|
||||
historyTimelines.forEach((timeline) => {
|
||||
const oldTimeline = cacheTimelines.find(
|
||||
(cacheTimeline) => cacheTimeline.entity_id === timeline.entity_id
|
||||
);
|
||||
if (oldTimeline) {
|
||||
oldTimeline.data = oldTimeline.data.concat(timeline.data);
|
||||
} else {
|
||||
cacheTimelines.push(timeline);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const pruneArray = (originalStartTime: Date, arr) => {
|
||||
if (arr.length === 0) {
|
||||
return arr;
|
||||
}
|
||||
const changedAfterStartTime = arr.findIndex(
|
||||
(state) => new Date(state.last_changed) > originalStartTime
|
||||
);
|
||||
if (changedAfterStartTime === 0) {
|
||||
// If all changes happened after originalStartTime then we are done.
|
||||
return arr;
|
||||
}
|
||||
|
||||
// If all changes happened at or before originalStartTime. Use last index.
|
||||
const updateIndex =
|
||||
changedAfterStartTime === -1 ? arr.length - 1 : changedAfterStartTime - 1;
|
||||
arr[updateIndex].last_changed = originalStartTime;
|
||||
return arr.slice(updateIndex);
|
||||
};
|
||||
|
||||
const pruneStartTime = (originalStartTime: Date, cacheData: HistoryResult) => {
|
||||
cacheData.line.forEach((line) => {
|
||||
line.data.forEach((entity) => {
|
||||
entity.states = pruneArray(originalStartTime, entity.states);
|
||||
});
|
||||
});
|
||||
|
||||
cacheData.timeline.forEach((timeline) => {
|
||||
timeline.data = pruneArray(originalStartTime, timeline.data);
|
||||
});
|
||||
};
|
@@ -84,12 +84,3 @@ export const setConversationOnboarding = (
|
||||
type: "conversation/onboarding/set",
|
||||
shown: value,
|
||||
});
|
||||
|
||||
export const prepareConversation = (
|
||||
hass: HomeAssistant,
|
||||
language?: string
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "conversation/prepare",
|
||||
language,
|
||||
});
|
||||
|
@@ -186,8 +186,8 @@ export interface EnergyInfo {
|
||||
|
||||
export interface EnergyValidationIssue {
|
||||
type: string;
|
||||
affected_entities: [string, unknown][];
|
||||
translation_placeholders: Record<string, string>;
|
||||
identifier: string;
|
||||
value?: unknown;
|
||||
}
|
||||
|
||||
export interface EnergyPreferencesValidation {
|
||||
@@ -200,12 +200,10 @@ export const getEnergyInfo = (hass: HomeAssistant) =>
|
||||
type: "energy/info",
|
||||
});
|
||||
|
||||
export const getEnergyPreferenceValidation = async (hass: HomeAssistant) => {
|
||||
await hass.loadBackendTranslation("issues", "energy");
|
||||
return hass.callWS<EnergyPreferencesValidation>({
|
||||
export const getEnergyPreferenceValidation = (hass: HomeAssistant) =>
|
||||
hass.callWS<EnergyPreferencesValidation>({
|
||||
type: "energy/validate",
|
||||
});
|
||||
};
|
||||
|
||||
export const getEnergyPreferences = (hass: HomeAssistant) =>
|
||||
hass.callWS<EnergyPreferences>({
|
||||
@@ -671,7 +669,7 @@ export const getEnergySolarForecasts = (hass: HomeAssistant) =>
|
||||
});
|
||||
|
||||
const energyGasUnitClass = ["volume", "energy"] as const;
|
||||
export type EnergyGasUnitClass = (typeof energyGasUnitClass)[number];
|
||||
export type EnergyGasUnitClass = typeof energyGasUnitClass[number];
|
||||
|
||||
export const getEnergyGasUnitClass = (
|
||||
prefs: EnergyPreferences,
|
||||
|
@@ -111,15 +111,6 @@ export const getExtendedEntityRegistryEntry = (
|
||||
entity_id: entityId,
|
||||
});
|
||||
|
||||
export const getExtendedEntityRegistryEntries = (
|
||||
hass: HomeAssistant,
|
||||
entityIds: string[]
|
||||
): Promise<Record<string, ExtEntityRegistryEntry>> =>
|
||||
hass.callWS({
|
||||
type: "config/entity_registry/get_entries",
|
||||
entity_ids: entityIds,
|
||||
});
|
||||
|
||||
export const updateEntityRegistryEntry = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
|
@@ -17,8 +17,6 @@ const NEED_ATTRIBUTE_DOMAINS = [
|
||||
"input_datetime",
|
||||
"thermostat",
|
||||
"water_heater",
|
||||
"person",
|
||||
"device_tracker",
|
||||
];
|
||||
const LINE_ATTRIBUTES_TO_KEEP = [
|
||||
"temperature",
|
||||
@@ -70,7 +68,7 @@ export interface HistoryStates {
|
||||
[entityId: string]: EntityHistoryState[];
|
||||
}
|
||||
|
||||
export interface EntityHistoryState {
|
||||
interface EntityHistoryState {
|
||||
/** state */
|
||||
s: string;
|
||||
/** attributes */
|
||||
@@ -81,12 +79,6 @@ export interface EntityHistoryState {
|
||||
lu: number;
|
||||
}
|
||||
|
||||
export interface HistoryStreamMessage {
|
||||
states: HistoryStates;
|
||||
start_time?: number; // Start time of this historical chunk
|
||||
end_time?: number; // End time of this historical chunk
|
||||
}
|
||||
|
||||
export const entityIdHistoryNeedsAttributes = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
@@ -94,6 +86,73 @@ export const entityIdHistoryNeedsAttributes = (
|
||||
!hass.states[entityId] ||
|
||||
NEED_ATTRIBUTE_DOMAINS.includes(computeDomain(entityId));
|
||||
|
||||
export const fetchRecent = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
startTime: Date,
|
||||
endTime: Date,
|
||||
skipInitialState = false,
|
||||
significantChangesOnly?: boolean,
|
||||
minimalResponse = true,
|
||||
noAttributes?: boolean
|
||||
): Promise<HassEntity[][]> => {
|
||||
let url = "history/period";
|
||||
if (startTime) {
|
||||
url += "/" + startTime.toISOString();
|
||||
}
|
||||
url += "?filter_entity_id=" + entityId;
|
||||
if (endTime) {
|
||||
url += "&end_time=" + endTime.toISOString();
|
||||
}
|
||||
if (skipInitialState) {
|
||||
url += "&skip_initial_state";
|
||||
}
|
||||
if (significantChangesOnly !== undefined) {
|
||||
url += `&significant_changes_only=${Number(significantChangesOnly)}`;
|
||||
}
|
||||
if (minimalResponse) {
|
||||
url += "&minimal_response";
|
||||
}
|
||||
if (noAttributes) {
|
||||
url += "&no_attributes";
|
||||
}
|
||||
return hass.callApi("GET", url);
|
||||
};
|
||||
|
||||
export const fetchRecentWS = (
|
||||
hass: HomeAssistant,
|
||||
entityIds: string[],
|
||||
startTime: Date,
|
||||
endTime: Date,
|
||||
skipInitialState = false,
|
||||
significantChangesOnly?: boolean,
|
||||
minimalResponse = true,
|
||||
noAttributes?: boolean
|
||||
) =>
|
||||
hass.callWS<HistoryStates>({
|
||||
type: "history/history_during_period",
|
||||
start_time: startTime.toISOString(),
|
||||
end_time: endTime.toISOString(),
|
||||
significant_changes_only: significantChangesOnly || false,
|
||||
include_start_time_state: !skipInitialState,
|
||||
minimal_response: minimalResponse,
|
||||
no_attributes: noAttributes || false,
|
||||
entity_ids: entityIds,
|
||||
});
|
||||
|
||||
export const fetchDate = (
|
||||
hass: HomeAssistant,
|
||||
startTime: Date,
|
||||
endTime: Date,
|
||||
entityIds: string[]
|
||||
): Promise<HassEntity[][]> =>
|
||||
hass.callApi(
|
||||
"GET",
|
||||
`history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}&minimal_response${
|
||||
entityIds ? `&filter_entity_id=${entityIds.join(",")}` : ``
|
||||
}`
|
||||
);
|
||||
|
||||
export const fetchDateWS = (
|
||||
hass: HomeAssistant,
|
||||
startTime: Date,
|
||||
@@ -115,142 +174,6 @@ export const fetchDateWS = (
|
||||
return hass.callWS<HistoryStates>(params);
|
||||
};
|
||||
|
||||
export const subscribeHistory = (
|
||||
hass: HomeAssistant,
|
||||
callbackFunction: (message: HistoryStreamMessage) => void,
|
||||
startTime: Date,
|
||||
endTime: Date,
|
||||
entityIds: string[]
|
||||
): Promise<() => Promise<void>> => {
|
||||
const params = {
|
||||
type: "history/stream",
|
||||
entity_ids: entityIds,
|
||||
start_time: startTime.toISOString(),
|
||||
end_time: endTime.toISOString(),
|
||||
minimal_response: true,
|
||||
no_attributes: !entityIds.some((entityId) =>
|
||||
entityIdHistoryNeedsAttributes(hass, entityId)
|
||||
),
|
||||
};
|
||||
return hass.connection.subscribeMessage<HistoryStreamMessage>(
|
||||
(message) => callbackFunction(message),
|
||||
params
|
||||
);
|
||||
};
|
||||
|
||||
class HistoryStream {
|
||||
hass: HomeAssistant;
|
||||
|
||||
hoursToShow: number;
|
||||
|
||||
combinedHistory: HistoryStates;
|
||||
|
||||
constructor(hass: HomeAssistant, hoursToShow: number) {
|
||||
this.hass = hass;
|
||||
this.hoursToShow = hoursToShow;
|
||||
this.combinedHistory = {};
|
||||
}
|
||||
|
||||
processMessage(streamMessage: HistoryStreamMessage): HistoryStates {
|
||||
if (!this.combinedHistory || !Object.keys(this.combinedHistory).length) {
|
||||
this.combinedHistory = streamMessage.states;
|
||||
return this.combinedHistory;
|
||||
}
|
||||
if (!Object.keys(streamMessage.states).length) {
|
||||
// Empty messages are still sent to
|
||||
// indicate no more historical events
|
||||
return this.combinedHistory;
|
||||
}
|
||||
const purgeBeforePythonTime =
|
||||
(new Date().getTime() - 60 * 60 * this.hoursToShow * 1000) / 1000;
|
||||
const newHistory: HistoryStates = {};
|
||||
for (const entityId of Object.keys(this.combinedHistory)) {
|
||||
newHistory[entityId] = [];
|
||||
}
|
||||
for (const entityId of Object.keys(streamMessage.states)) {
|
||||
newHistory[entityId] = [];
|
||||
}
|
||||
for (const entityId of Object.keys(newHistory)) {
|
||||
if (
|
||||
entityId in this.combinedHistory &&
|
||||
entityId in streamMessage.states
|
||||
) {
|
||||
const entityCombinedHistory = this.combinedHistory[entityId];
|
||||
const lastEntityCombinedHistory =
|
||||
entityCombinedHistory[entityCombinedHistory.length - 1];
|
||||
newHistory[entityId] = entityCombinedHistory.concat(
|
||||
streamMessage.states[entityId]
|
||||
);
|
||||
if (
|
||||
streamMessage.states[entityId][0].lu < lastEntityCombinedHistory.lu
|
||||
) {
|
||||
// If the history is out of order we have to sort it.
|
||||
newHistory[entityId] = newHistory[entityId].sort(
|
||||
(a, b) => a.lu - b.lu
|
||||
);
|
||||
}
|
||||
} else if (entityId in this.combinedHistory) {
|
||||
newHistory[entityId] = this.combinedHistory[entityId];
|
||||
} else {
|
||||
newHistory[entityId] = streamMessage.states[entityId];
|
||||
}
|
||||
// Remove old history
|
||||
if (entityId in this.combinedHistory) {
|
||||
const expiredStates = newHistory[entityId].filter(
|
||||
(state) => state.lu < purgeBeforePythonTime
|
||||
);
|
||||
if (!expiredStates.length) {
|
||||
continue;
|
||||
}
|
||||
newHistory[entityId] = newHistory[entityId].filter(
|
||||
(state) => state.lu >= purgeBeforePythonTime
|
||||
);
|
||||
if (
|
||||
newHistory[entityId].length &&
|
||||
newHistory[entityId][0].lu === purgeBeforePythonTime
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
// Update the first entry to the start time state
|
||||
// as we need to preserve the start time state and
|
||||
// only expire the rest of the history as it ages.
|
||||
const lastExpiredState = expiredStates[expiredStates.length - 1];
|
||||
lastExpiredState.lu = purgeBeforePythonTime;
|
||||
newHistory[entityId].unshift(lastExpiredState);
|
||||
}
|
||||
}
|
||||
this.combinedHistory = newHistory;
|
||||
return this.combinedHistory;
|
||||
}
|
||||
}
|
||||
|
||||
export const subscribeHistoryStatesTimeWindow = (
|
||||
hass: HomeAssistant,
|
||||
callbackFunction: (data: HistoryStates) => void,
|
||||
hoursToShow: number,
|
||||
entityIds: string[],
|
||||
minimalResponse = true,
|
||||
significantChangesOnly = true
|
||||
): Promise<() => Promise<void>> => {
|
||||
const params = {
|
||||
type: "history/stream",
|
||||
entity_ids: entityIds,
|
||||
start_time: new Date(
|
||||
new Date().getTime() - 60 * 60 * hoursToShow * 1000
|
||||
).toISOString(),
|
||||
minimal_response: minimalResponse,
|
||||
significant_changes_only: significantChangesOnly,
|
||||
no_attributes: !entityIds.some((entityId) =>
|
||||
entityIdHistoryNeedsAttributes(hass, entityId)
|
||||
),
|
||||
};
|
||||
const stream = new HistoryStream(hass, hoursToShow);
|
||||
return hass.connection.subscribeMessage<HistoryStreamMessage>(
|
||||
(message) => callbackFunction(stream.processMessage(message)),
|
||||
params
|
||||
);
|
||||
};
|
||||
|
||||
const equalState = (obj1: LineChartState, obj2: LineChartState) =>
|
||||
obj1.state === obj2.state &&
|
||||
// Only compare attributes if both states have an attributes object.
|
||||
|
@@ -7,8 +7,8 @@ import { TranslationDict } from "../types";
|
||||
import { UNAVAILABLE_STATES } from "./entity";
|
||||
|
||||
type HumidifierState =
|
||||
| (typeof FIXED_DOMAIN_STATES.humidifier)[number]
|
||||
| (typeof UNAVAILABLE_STATES)[number];
|
||||
| typeof FIXED_DOMAIN_STATES.humidifier[number]
|
||||
| typeof UNAVAILABLE_STATES[number];
|
||||
type HumidifierMode =
|
||||
keyof TranslationDict["state_attributes"]["humidifier"]["mode"];
|
||||
|
||||
|
@@ -98,7 +98,7 @@ const statisticTypes = [
|
||||
"state",
|
||||
"sum",
|
||||
] as const;
|
||||
export type StatisticsTypes = (typeof statisticTypes)[number][];
|
||||
export type StatisticsTypes = typeof statisticTypes[number][];
|
||||
|
||||
export interface StatisticsValidationResults {
|
||||
[statisticId: string]: StatisticsValidationResult[];
|
||||
|
@@ -15,7 +15,7 @@ export interface ScheduleDay {
|
||||
to: string;
|
||||
}
|
||||
|
||||
type ScheduleDays = { [K in (typeof weekdays)[number]]?: ScheduleDay[] };
|
||||
type ScheduleDays = { [K in typeof weekdays[number]]?: ScheduleDay[] };
|
||||
|
||||
export interface Schedule extends ScheduleDays {
|
||||
id: string;
|
||||
|
@@ -77,7 +77,7 @@ const activateSceneActionStruct: Describe<ServiceSceneAction> = assign(
|
||||
export interface ScriptEntity extends HassEntityBase {
|
||||
attributes: HassEntityAttributeBase & {
|
||||
last_triggered: string;
|
||||
mode: (typeof MODES)[number];
|
||||
mode: typeof MODES[number];
|
||||
current?: number;
|
||||
max?: number;
|
||||
};
|
||||
@@ -89,7 +89,7 @@ export interface ManualScriptConfig {
|
||||
alias: string;
|
||||
sequence: Action | Action[];
|
||||
icon?: string;
|
||||
mode?: (typeof MODES)[number];
|
||||
mode?: typeof MODES[number];
|
||||
max?: number;
|
||||
}
|
||||
|
||||
|
@@ -1,15 +1,2 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export const SENSOR_DEVICE_CLASS_BATTERY = "battery";
|
||||
export const SENSOR_DEVICE_CLASS_TIMESTAMP = "timestamp";
|
||||
|
||||
export type SensorDeviceClassUnits = { units: string[] };
|
||||
|
||||
export const getSensorDeviceClassConvertibleUnits = (
|
||||
hass: HomeAssistant,
|
||||
deviceClass: string
|
||||
): Promise<SensorDeviceClassUnits> =>
|
||||
hass.callWS({
|
||||
type: "sensor/device_class_convertible_units",
|
||||
device_class: deviceClass,
|
||||
});
|
||||
|
@@ -1,11 +0,0 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface ThreadInfo {
|
||||
url: string;
|
||||
active_dataset_tlvs: string;
|
||||
}
|
||||
|
||||
export const threadGetInfo = (hass: HomeAssistant): Promise<ThreadInfo> =>
|
||||
hass.callWS({
|
||||
type: "otbr/info",
|
||||
});
|
@@ -3,12 +3,10 @@ import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { throttle } from "../../common/util/throttle";
|
||||
import "../../components/chart/state-history-charts";
|
||||
import {
|
||||
HistoryResult,
|
||||
subscribeHistoryStatesTimeWindow,
|
||||
computeHistory,
|
||||
} from "../../data/history";
|
||||
import { getRecentWithCache } from "../../data/cached-history";
|
||||
import { HistoryResult } from "../../data/history";
|
||||
import {
|
||||
fetchStatistics,
|
||||
getStatisticMetadata,
|
||||
@@ -41,11 +39,9 @@ export class MoreInfoHistory extends LitElement {
|
||||
|
||||
private _statNames?: Record<string, string>;
|
||||
|
||||
private _interval?: number;
|
||||
|
||||
private _subscribed?: Promise<(() => Promise<void>) | void>;
|
||||
|
||||
private _error?: string;
|
||||
private _throttleGetStateHistory = throttle(() => {
|
||||
this._getStateHistory();
|
||||
}, 10000);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.entityId) {
|
||||
@@ -63,9 +59,7 @@ export class MoreInfoHistory extends LitElement {
|
||||
)}</a
|
||||
>
|
||||
</div>
|
||||
${this._error
|
||||
? html`<div class="errors">${this._error}</div>`
|
||||
: this._statistics
|
||||
${this._statistics
|
||||
? html`<statistics-chart
|
||||
.hass=${this.hass}
|
||||
.isLoadingData=${!this._statistics}
|
||||
@@ -100,45 +94,24 @@ export class MoreInfoHistory extends LitElement {
|
||||
this.entityId
|
||||
}&start_date=${startOfYesterday().toISOString()}`;
|
||||
|
||||
this._getStateHistory();
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated && this.entityId) {
|
||||
this._getStateHistory();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribeHistoryTimeWindow();
|
||||
}
|
||||
|
||||
private _unsubscribeHistoryTimeWindow() {
|
||||
if (!this._subscribed) {
|
||||
this._throttleGetStateHistory();
|
||||
return;
|
||||
}
|
||||
clearInterval(this._interval);
|
||||
this._subscribed.then((unsubscribe) => {
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
}
|
||||
this._subscribed = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
private _redrawGraph() {
|
||||
if (this._stateHistory) {
|
||||
this._stateHistory = { ...this._stateHistory };
|
||||
if (this._statistics || !this.entityId || !changedProps.has("hass")) {
|
||||
// Don't update statistics on a state update, as they only update every 5 minutes.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private _setRedrawTimer() {
|
||||
// redraw the graph every minute to update the time axis
|
||||
clearInterval(this._interval);
|
||||
this._interval = window.setInterval(() => this._redrawGraph(), 1000 * 60);
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
|
||||
if (
|
||||
oldHass &&
|
||||
this.hass.states[this.entityId] !== oldHass?.states[this.entityId]
|
||||
) {
|
||||
// wait for commit of data (we only account for the default setting of 1 sec)
|
||||
setTimeout(this._throttleGetStateHistory, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
private async _getStateHistory(): Promise<void> {
|
||||
@@ -161,32 +134,19 @@ export class MoreInfoHistory extends LitElement {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!isComponentLoaded(this.hass, "history") || this._subscribed) {
|
||||
if (!isComponentLoaded(this.hass, "history")) {
|
||||
return;
|
||||
}
|
||||
if (this._subscribed) {
|
||||
this._unsubscribeHistoryTimeWindow();
|
||||
}
|
||||
this._subscribed = subscribeHistoryStatesTimeWindow(
|
||||
this._stateHistory = await getRecentWithCache(
|
||||
this.hass!,
|
||||
(combinedHistory) => {
|
||||
if (!this._subscribed) {
|
||||
// Message came in before we had a chance to unload
|
||||
return;
|
||||
}
|
||||
this._stateHistory = computeHistory(
|
||||
this.hass!,
|
||||
combinedHistory,
|
||||
this.hass!.localize
|
||||
);
|
||||
[this.entityId],
|
||||
{
|
||||
cacheKey: `more_info.${this.entityId}`,
|
||||
hoursToShow: 24,
|
||||
},
|
||||
24,
|
||||
[this.entityId]
|
||||
).catch((err) => {
|
||||
this._subscribed = undefined;
|
||||
this._error = err;
|
||||
});
|
||||
this._setRedrawTimer();
|
||||
this.hass!.localize,
|
||||
this.hass!.language
|
||||
);
|
||||
}
|
||||
|
||||
private _close(): void {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable lit/prefer-static-styles */
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiClose, mdiMicrophone, mdiSend } from "@mdi/js";
|
||||
import { mdiMicrophone } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -15,14 +15,12 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { SpeechRecognition } from "../../common/dom/speech-recognition";
|
||||
import "../../components/ha-dialog";
|
||||
import type { HaDialog } from "../../components/ha-dialog";
|
||||
import "../../components/ha-header-bar";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../components/ha-textfield";
|
||||
import {
|
||||
AgentInfo,
|
||||
getAgentInfo,
|
||||
prepareConversation,
|
||||
processConversationInput,
|
||||
setConversationOnboarding,
|
||||
} from "../../data/conversation";
|
||||
@@ -57,11 +55,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
|
||||
@state() private _agentInfo?: AgentInfo;
|
||||
|
||||
@state() private _showSendButton = false;
|
||||
|
||||
@query("#scroll-container") private _scrollContainer!: HaDialog;
|
||||
|
||||
@query("#message-input") private _messageInput!: HaTextField;
|
||||
@query("ha-dialog", true) private _dialog!: HaDialog;
|
||||
|
||||
private recognition!: SpeechRecognition;
|
||||
|
||||
@@ -69,8 +63,10 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
|
||||
public async showDialog(): Promise<void> {
|
||||
this._opened = true;
|
||||
if (SpeechRecognition) {
|
||||
this._startListening();
|
||||
}
|
||||
this._agentInfo = await getAgentInfo(this.hass);
|
||||
this._scrollMessagesBottom();
|
||||
}
|
||||
|
||||
public async closeDialog(): Promise<void> {
|
||||
@@ -86,96 +82,63 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${this.hass.localize("ui.dialogs.voice_command.title")}
|
||||
flexContent
|
||||
>
|
||||
<div slot="heading">
|
||||
<ha-header-bar>
|
||||
<span slot="title">
|
||||
${this.hass.localize("ui.dialogs.voice_command.title")}
|
||||
</span>
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
dialogAction="cancel"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</ha-header-bar>
|
||||
</div>
|
||||
<div class="messages">
|
||||
<div class="messages-container" id="scroll-container">
|
||||
${this._agentInfo && this._agentInfo.onboarding
|
||||
? html`
|
||||
<div class="onboarding">
|
||||
${this._agentInfo.onboarding.text}
|
||||
<div
|
||||
class="side-by-side"
|
||||
@click=${this._completeOnboarding}
|
||||
<ha-dialog open @closed=${this.closeDialog}>
|
||||
<div>
|
||||
${this._agentInfo && this._agentInfo.onboarding
|
||||
? html`
|
||||
<div class="onboarding">
|
||||
${this._agentInfo.onboarding.text}
|
||||
<div class="side-by-side" @click=${this._completeOnboarding}>
|
||||
<a
|
||||
class="button"
|
||||
href=${this._agentInfo.onboarding.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
><mwc-button unelevated
|
||||
>${this.hass.localize("ui.common.yes")}!</mwc-button
|
||||
></a
|
||||
>
|
||||
<mwc-button outlined
|
||||
>${this.hass.localize("ui.common.no")}</mwc-button
|
||||
>
|
||||
<a
|
||||
class="button"
|
||||
href=${this._agentInfo.onboarding.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
><mwc-button unelevated
|
||||
>${this.hass.localize("ui.common.yes")}!</mwc-button
|
||||
></a
|
||||
>
|
||||
<mwc-button outlined
|
||||
>${this.hass.localize("ui.common.no")}</mwc-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this._conversation.map(
|
||||
(message) => html`
|
||||
<div class=${this._computeMessageClasses(message)}>
|
||||
${message.text}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
${this.results
|
||||
? html`
|
||||
<div class="message user">
|
||||
<span
|
||||
class=${classMap({
|
||||
interimTranscript: !this.results.final,
|
||||
})}
|
||||
>${this.results.transcript}</span
|
||||
>${!this.results.final ? "…" : ""}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
: ""}
|
||||
${this._conversation.map(
|
||||
(message) => html`
|
||||
<div class=${this._computeMessageClasses(message)}>
|
||||
${message.text}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
${this.results
|
||||
? html`
|
||||
<div class="message user">
|
||||
<span
|
||||
class=${classMap({
|
||||
interimTranscript: !this.results.final,
|
||||
})}
|
||||
>${this.results.transcript}</span
|
||||
>${!this.results.final ? "…" : ""}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="input" slot="primaryAction">
|
||||
<ha-textfield
|
||||
id="message-input"
|
||||
@keyup=${this._handleKeyUp}
|
||||
@input=${this._handleInput}
|
||||
.label=${this.hass.localize(`ui.dialogs.voice_command.input_label`)}
|
||||
.label=${this.hass.localize(
|
||||
`ui.dialogs.voice_command.${
|
||||
SpeechRecognition ? "label_voice" : "label"
|
||||
}`
|
||||
)}
|
||||
dialogInitialFocus
|
||||
iconTrailing
|
||||
>
|
||||
<span slot="trailingIcon">
|
||||
${this._showSendButton
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class="listening-icon"
|
||||
.path=${mdiSend}
|
||||
@click=${this._handleSendMessage}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.voice_command.send_text"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: SpeechRecognition
|
||||
? html`
|
||||
${SpeechRecognition
|
||||
? html`
|
||||
<span slot="trailingIcon">
|
||||
${this.results
|
||||
? html`
|
||||
<div class="bouncer">
|
||||
@@ -185,17 +148,13 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
<ha-icon-button
|
||||
class="listening-icon"
|
||||
.path=${mdiMicrophone}
|
||||
@click=${this._toggleListening}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.voice_command.start_listening"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</span>
|
||||
</span>
|
||||
`
|
||||
: ""}
|
||||
</ha-textfield>
|
||||
${this._agentInfo && this._agentInfo.attribution
|
||||
? html`
|
||||
@@ -221,7 +180,6 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
text: this.hass.localize("ui.dialogs.voice_command.how_can_i_help"),
|
||||
},
|
||||
];
|
||||
prepareConversation(this.hass, this.hass.language);
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
@@ -240,24 +198,6 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
if (ev.keyCode === 13 && input.value) {
|
||||
this._processText(input.value);
|
||||
input.value = "";
|
||||
this._showSendButton = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleInput(ev: InputEvent) {
|
||||
const value = (ev.target as HaTextField).value;
|
||||
if (value && !this._showSendButton) {
|
||||
this._showSendButton = true;
|
||||
} else if (!value && this._showSendButton) {
|
||||
this._showSendButton = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleSendMessage() {
|
||||
if (this._messageInput.value) {
|
||||
this._processText(this._messageInput.value);
|
||||
this._messageInput.value = "";
|
||||
this._showSendButton = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,7 +210,6 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
this.recognition = new SpeechRecognition();
|
||||
this.recognition.interimResults = true;
|
||||
this.recognition.lang = this.hass.language;
|
||||
this.recognition.continuous = false;
|
||||
|
||||
this.recognition.addEventListener("start", () => {
|
||||
this.results = {
|
||||
@@ -369,13 +308,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
if (!this.results) {
|
||||
this._startListening();
|
||||
} else {
|
||||
this._stopListening();
|
||||
}
|
||||
}
|
||||
|
||||
private _stopListening() {
|
||||
if (this.recognition) {
|
||||
this.recognition.stop();
|
||||
this.recognition!.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,7 +329,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
}
|
||||
|
||||
private _scrollMessagesBottom() {
|
||||
this._scrollContainer.scrollTo(0, 99999);
|
||||
this._dialog.scrollToPos(0, 99999);
|
||||
}
|
||||
|
||||
private _computeMessageClasses(message: Message) {
|
||||
@@ -407,7 +340,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-icon-button.listening-icon {
|
||||
ha-icon-button {
|
||||
color: var(--secondary-text-color);
|
||||
margin-right: -24px;
|
||||
margin-inline-end: -24px;
|
||||
@@ -415,7 +348,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
ha-icon-button.listening-icon[active] {
|
||||
ha-icon-button[active] {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
@@ -423,19 +356,9 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
--primary-action-button-flex: 1;
|
||||
--secondary-action-button-flex: 0;
|
||||
--mdc-dialog-max-width: 450px;
|
||||
--mdc-dialog-max-height: 500px;
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
ha-textfield {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
a.button {
|
||||
text-decoration: none;
|
||||
@@ -457,25 +380,6 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
.attribution {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.messages {
|
||||
display: block;
|
||||
height: 300px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.messages {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.messages-container {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
left: 0px;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
max-height: 100%;
|
||||
}
|
||||
.message {
|
||||
font-size: 18px;
|
||||
clear: both;
|
||||
|
@@ -26,7 +26,7 @@ class DialogAutomationMode extends LitElement implements HassDialog {
|
||||
|
||||
private _params!: AutomationModeDialog;
|
||||
|
||||
@state() private _newMode: (typeof MODES)[number] = AUTOMATION_DEFAULT_MODE;
|
||||
@state() private _newMode: typeof MODES[number] = AUTOMATION_DEFAULT_MODE;
|
||||
|
||||
@state() private _newMax?: number;
|
||||
|
||||
|
@@ -40,8 +40,7 @@ import {
|
||||
} from "../../../../data/cloud";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
ExtEntityRegistryEntry,
|
||||
getExtendedEntityRegistryEntries,
|
||||
getExtendedEntityRegistryEntry,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../../../data/entity_registry";
|
||||
import {
|
||||
@@ -69,8 +68,6 @@ class CloudGoogleAssistant extends LitElement {
|
||||
|
||||
@state() private _entities?: GoogleEntity[];
|
||||
|
||||
@state() private _entries?: { [id: string]: ExtEntityRegistryEntry };
|
||||
|
||||
@state() private _syncing = false;
|
||||
|
||||
@state()
|
||||
@@ -167,8 +164,6 @@ class CloudGoogleAssistant extends LitElement {
|
||||
: mdiCloseBoxMultiple}
|
||||
></ha-icon-button>`;
|
||||
|
||||
const aliases = this._entries?.[entity.entity_id]?.aliases;
|
||||
|
||||
target.push(html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
@@ -179,51 +174,17 @@ class CloudGoogleAssistant extends LitElement {
|
||||
secondary-line
|
||||
@click=${this._showMoreInfo}
|
||||
>
|
||||
${aliases
|
||||
? html`
|
||||
<span>
|
||||
${aliases.length > 0
|
||||
? [...aliases]
|
||||
.sort((a, b) =>
|
||||
stringCompare(a, b, this.hass.locale.language)
|
||||
)
|
||||
.join(", ")
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.cloud.google.no_aliases"
|
||||
)}
|
||||
</span>
|
||||
<br />
|
||||
<button
|
||||
class="link"
|
||||
.entityId=${entity.entity_id}
|
||||
@click=${this._openAliasesSettings}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.cloud.google.${
|
||||
aliases.length > 0
|
||||
? "manage_aliases"
|
||||
: "add_aliases"
|
||||
}`
|
||||
)}
|
||||
</button>
|
||||
`
|
||||
: html`
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.google.aliases_not_available"
|
||||
)}
|
||||
</span>
|
||||
<br />
|
||||
<button
|
||||
class="link"
|
||||
.stateObj=${stateObj}
|
||||
@click=${this._showMoreInfoSettings}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.google.aliases_not_available_learn_more"
|
||||
)}
|
||||
</button>
|
||||
`}
|
||||
${entity.entity_id in this.hass.entities
|
||||
? html`<button
|
||||
class="link"
|
||||
.entityId=${entity.entity_id}
|
||||
@click=${this._openAliasesSettings}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.google.manage_aliases"
|
||||
)}
|
||||
</button>`
|
||||
: ""}
|
||||
</state-info>
|
||||
${!emptyFilter
|
||||
? html`${iconButton}`
|
||||
@@ -418,19 +379,14 @@ class CloudGoogleAssistant extends LitElement {
|
||||
private async _openAliasesSettings(ev) {
|
||||
ev.stopPropagation();
|
||||
const entityId = ev.target.entityId;
|
||||
const entry = this._entries![entityId];
|
||||
const entry = await getExtendedEntityRegistryEntry(this.hass, entityId);
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
showEntityAliasesDialog(this, {
|
||||
entity: entry,
|
||||
updateEntry: async (updates) => {
|
||||
const { entity_entry } = await updateEntityRegistryEntry(
|
||||
this.hass,
|
||||
entry.entity_id,
|
||||
updates
|
||||
);
|
||||
this._entries![entity_entry.entity_id] = entity_entry;
|
||||
await updateEntityRegistryEntry(this.hass, entry.entity_id, updates);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -459,13 +415,6 @@ class CloudGoogleAssistant extends LitElement {
|
||||
|
||||
private async _fetchData() {
|
||||
const entities = await fetchCloudGoogleEntities(this.hass);
|
||||
this._entries = await getExtendedEntityRegistryEntries(
|
||||
this.hass,
|
||||
entities
|
||||
.filter((ent) => this.hass.entities[ent.entity_id])
|
||||
.map((e) => e.entity_id)
|
||||
);
|
||||
|
||||
entities.sort((a, b) => {
|
||||
const stateA = this.hass.states[a.entity_id];
|
||||
const stateB = this.hass.states[b.entity_id];
|
||||
@@ -480,14 +429,7 @@ class CloudGoogleAssistant extends LitElement {
|
||||
|
||||
private _showMoreInfo(ev) {
|
||||
const entityId = ev.currentTarget.stateObj.entity_id;
|
||||
const moreInfoTab = ev.currentTarget.moreInfoTab;
|
||||
fireEvent(this, "hass-more-info", { entityId, tab: moreInfoTab });
|
||||
}
|
||||
|
||||
private _showMoreInfoSettings(ev) {
|
||||
ev.stopPropagation();
|
||||
const entityId = ev.currentTarget.stateObj.entity_id;
|
||||
fireEvent(this, "hass-more-info", { entityId, tab: "settings" });
|
||||
fireEvent(this, "hass-more-info", { entityId });
|
||||
}
|
||||
|
||||
private async _exposeChanged(ev: CustomEvent<ActionDetail>) {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { groupBy } from "../../../../common/util/group-by";
|
||||
import "../../../../components/ha-alert";
|
||||
import { EnergyValidationIssue } from "../../../../data/energy";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
@@ -17,33 +18,46 @@ class EnergyValidationMessage extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return this.issues.map(
|
||||
(issue) => html`
|
||||
<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${this.hass.localize(
|
||||
`component.energy.issues.${issue.type}.title`
|
||||
) || issue.type}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`component.energy.issues.${issue.type}.description`,
|
||||
issue.translation_placeholders
|
||||
)}
|
||||
${issue.type === "recorder_untracked"
|
||||
? html`(<a
|
||||
href="https://www.home-assistant.io/integrations/recorder#configure-filter"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>${this.hass.localize("ui.panel.config.common.learn_more")}</a
|
||||
>)`
|
||||
: ""}
|
||||
<ul>
|
||||
${issue.affected_entities.map(
|
||||
([entity, value]) =>
|
||||
html`<li>${entity}${value ? html` (${value})` : ""}</li>`
|
||||
const grouped = groupBy(this.issues, (issue) => issue.type);
|
||||
|
||||
return Object.entries(grouped).map(
|
||||
([issueType, gIssues]) => html`
|
||||
<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${
|
||||
this.hass.localize(
|
||||
`ui.panel.config.energy.validation.issues.${issueType}.title`
|
||||
) || issueType
|
||||
}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.energy.validation.issues.${issueType}.description`,
|
||||
{ currency: this.hass.config.currency }
|
||||
)}
|
||||
</ul>
|
||||
</ha-alert>
|
||||
${
|
||||
issueType === "recorder_untracked"
|
||||
? html`(<a
|
||||
href="https://www.home-assistant.io/integrations/recorder#configure-filter"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.common.learn_more"
|
||||
)}</a
|
||||
>)`
|
||||
: ""
|
||||
}
|
||||
<ul>
|
||||
${gIssues.map(
|
||||
(issue) =>
|
||||
html`<li>
|
||||
${issue.identifier}${issue.value
|
||||
? html` (${issue.value})`
|
||||
: ""}
|
||||
</li>`
|
||||
)}
|
||||
</ul>
|
||||
</ha-alert>
|
||||
</div>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
@@ -30,7 +30,7 @@ class DialogEntityAliases extends LitElement {
|
||||
this._error = undefined;
|
||||
this._aliases =
|
||||
this._params.entity.aliases?.length > 0
|
||||
? [...this._params.entity.aliases].sort()
|
||||
? this._params.entity.aliases
|
||||
: [""];
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
@@ -7,7 +7,6 @@ import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import "../../../components/ha-area-picker";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-radio";
|
||||
@@ -286,11 +285,7 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
"ui.dialogs.entity_registry.editor.no_aliases"
|
||||
)}
|
||||
</span>
|
||||
<span slot="secondary">
|
||||
${[...this.entry.aliases]
|
||||
.sort((a, b) => stringCompare(a, b, this.hass.locale.language))
|
||||
.join(", ")}
|
||||
</span>
|
||||
<span slot="secondary">${this.entry.aliases.join(", ")}</span>
|
||||
<ha-svg-icon slot="meta" .path=${mdiPencil}></ha-svg-icon>
|
||||
</mwc-list-item>
|
||||
</mwc-list>
|
||||
|
@@ -67,7 +67,6 @@ import {
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../../data/entity_registry";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import { getSensorDeviceClassConvertibleUnits } from "../../../data/sensor";
|
||||
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@@ -118,6 +117,58 @@ const OVERRIDE_NUMBER_UNITS = {
|
||||
temperature: ["°C", "°F", "K"],
|
||||
};
|
||||
|
||||
const OVERRIDE_SENSOR_UNITS = {
|
||||
current: ["A", "mA"],
|
||||
data_rate: [
|
||||
"bit/s",
|
||||
"kbit/s",
|
||||
"Mbit/s",
|
||||
"Gbit/s",
|
||||
"B/s",
|
||||
"kB/s",
|
||||
"MB/s",
|
||||
"GB/s",
|
||||
"KiB/s",
|
||||
"MiB/s",
|
||||
"GiB/s",
|
||||
],
|
||||
data_size: [
|
||||
"bit",
|
||||
"kbit",
|
||||
"Mbit",
|
||||
"Gbit",
|
||||
"B",
|
||||
"kB",
|
||||
"MB",
|
||||
"GB",
|
||||
"TB",
|
||||
"PB",
|
||||
"EB",
|
||||
"ZB",
|
||||
"YB",
|
||||
"KiB",
|
||||
"MiB",
|
||||
"GiB",
|
||||
"TiB",
|
||||
"PiB",
|
||||
"EiB",
|
||||
"ZiB",
|
||||
"YiB",
|
||||
],
|
||||
distance: ["cm", "ft", "in", "km", "m", "mi", "mm", "yd"],
|
||||
gas: ["CCF", "ft³", "m³"],
|
||||
precipitation: ["cm", "in", "mm"],
|
||||
precipitation_intensity: ["in/d", "in/h", "mm/d", "mm/h"],
|
||||
pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"],
|
||||
speed: ["ft/s", "in/d", "in/h", "km/h", "kn", "m/s", "mm/d", "mm/h", "mph"],
|
||||
temperature: ["°C", "°F", "K"],
|
||||
voltage: ["V", "mV"],
|
||||
volume: ["CCF", "fl. oz.", "ft³", "gal", "L", "mL", "m³"],
|
||||
water: ["CCF", "ft³", "gal", "L", "m³"],
|
||||
weight: ["g", "kg", "lb", "mg", "oz", "st", "µg"],
|
||||
wind_speed: ["ft/s", "km/h", "kn", "mph", "m/s"],
|
||||
};
|
||||
|
||||
const OVERRIDE_WEATHER_UNITS = {
|
||||
precipitation: ["mm", "in"],
|
||||
pressure: ["hPa", "mbar", "mmHg", "inHg"],
|
||||
@@ -172,8 +223,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _cameraPrefs?: CameraPreferences;
|
||||
|
||||
@state() private _sensorDeviceClassConvertibleUnits?: string[];
|
||||
|
||||
private _origEntityId!: string;
|
||||
|
||||
private _deviceLookup?: Record<string, DeviceRegistryEntry>;
|
||||
@@ -277,22 +326,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
protected async updated(changedProps: PropertyValues): Promise<void> {
|
||||
if (changedProps.has("_deviceClass")) {
|
||||
const domain = computeDomain(this.entry.entity_id);
|
||||
|
||||
if (domain === "sensor" && this._deviceClass) {
|
||||
const { units } = await getSensorDeviceClassConvertibleUnits(
|
||||
this.hass,
|
||||
this._deviceClass
|
||||
);
|
||||
this._sensorDeviceClassConvertibleUnits = units;
|
||||
} else {
|
||||
this._sensorDeviceClassConvertibleUnits = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this.entry.entity_id !== this._origEntityId) {
|
||||
return html``;
|
||||
@@ -437,7 +470,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
${domain === "sensor" &&
|
||||
this._deviceClass &&
|
||||
stateObj?.attributes.unit_of_measurement &&
|
||||
this._sensorDeviceClassConvertibleUnits?.includes(
|
||||
OVERRIDE_SENSOR_UNITS[this._deviceClass]?.includes(
|
||||
stateObj?.attributes.unit_of_measurement
|
||||
)
|
||||
? html`
|
||||
@@ -451,7 +484,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
@selected=${this._unitChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${this._sensorDeviceClassConvertibleUnits.map(
|
||||
${OVERRIDE_SENSOR_UNITS[this._deviceClass].map(
|
||||
(unit: string) => html`
|
||||
<mwc-list-item .value=${unit}>${unit}</mwc-list-item>
|
||||
`
|
||||
@@ -786,13 +819,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
"ui.dialogs.entity_registry.editor.no_aliases"
|
||||
)}
|
||||
</span>
|
||||
<span slot="secondary">
|
||||
${[...this.entry.aliases]
|
||||
.sort((a, b) =>
|
||||
stringCompare(a, b, this.hass.locale.language)
|
||||
)
|
||||
.join(", ")}
|
||||
</span>
|
||||
<span slot="secondary">${this.entry.aliases.join(", ")}</span>
|
||||
<ha-svg-icon slot="meta" .path=${mdiPencil}></ha-svg-icon>
|
||||
</mwc-list-item>
|
||||
</mwc-list>
|
||||
|
@@ -485,13 +485,6 @@ class HaPanelConfig extends HassRouterPage {
|
||||
"./integrations/integration-panels/matter/matter-config-panel"
|
||||
),
|
||||
},
|
||||
thread: {
|
||||
tag: "thread-config-panel",
|
||||
load: () =>
|
||||
import(
|
||||
"./integrations/integration-panels/thread/thread-config-panel"
|
||||
),
|
||||
},
|
||||
application_credentials: {
|
||||
tag: "ha-config-application-credentials",
|
||||
load: () =>
|
||||
|
@@ -39,7 +39,6 @@ import {
|
||||
rebootHost,
|
||||
shutdownHost,
|
||||
} from "../../../data/hassio/host";
|
||||
import { scanUSBDevices } from "../../../data/usb";
|
||||
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@@ -220,10 +219,6 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._configEntries) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
let boardId: string | undefined;
|
||||
let boardName: string | undefined;
|
||||
let imageURL: string | undefined;
|
||||
@@ -235,22 +230,13 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
const dongles = this._hardwareInfo?.hardware.filter(
|
||||
(hw) =>
|
||||
hw.dongle !== null &&
|
||||
(!hw.config_entries.length ||
|
||||
hw.config_entries.some(
|
||||
(entryId) =>
|
||||
this._configEntries![entryId] &&
|
||||
!this._configEntries![entryId].disabled_by
|
||||
))
|
||||
(hw) => hw.dongle !== null
|
||||
);
|
||||
|
||||
if (boardData) {
|
||||
boardConfigEntries = boardData.config_entries
|
||||
.map((id) => this._configEntries![id])
|
||||
.filter(
|
||||
(entry) => entry?.supports_options && !entry.disabled_by
|
||||
) as ConfigEntry[];
|
||||
.map((id) => this._configEntries?.[id])
|
||||
.filter((entry) => entry?.supports_options) as ConfigEntry[];
|
||||
boardId = boardData.board!.hassio_board_id;
|
||||
boardName = boardData.name;
|
||||
documentationURL = boardData.url;
|
||||
@@ -376,10 +362,8 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
|
||||
? html`<ha-card>
|
||||
${dongles.map((dongle) => {
|
||||
const configEntry = dongle.config_entries
|
||||
.map((id) => this._configEntries![id])
|
||||
.filter(
|
||||
(entry) => entry?.supports_options && !entry.disabled_by
|
||||
)[0];
|
||||
.map((id) => this._configEntries?.[id])
|
||||
.filter((entry) => entry?.supports_options)[0];
|
||||
return html`<div class="row">
|
||||
${dongle.name}${configEntry
|
||||
? html`<mwc-button
|
||||
@@ -460,10 +444,6 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private async _load() {
|
||||
if (isComponentLoaded(this.hass, "usb")) {
|
||||
await scanUSBDevices(this.hass);
|
||||
}
|
||||
|
||||
const isHassioLoaded = isComponentLoaded(this.hass, "hassio");
|
||||
try {
|
||||
if (isComponentLoaded(this.hass, "hardware")) {
|
||||
|
@@ -21,7 +21,7 @@ export const HELPER_DOMAINS = [
|
||||
"schedule",
|
||||
] as const;
|
||||
|
||||
export type HelperDomain = (typeof HELPER_DOMAINS)[number];
|
||||
export type HelperDomain = typeof HELPER_DOMAINS[number];
|
||||
export const isHelperDomain = arrayLiteralIncludes(HELPER_DOMAINS);
|
||||
|
||||
export type Helper =
|
||||
|
@@ -145,10 +145,7 @@ class HaInputTextForm extends LitElement {
|
||||
.configValue=${"pattern"}
|
||||
@input=${this._valueChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.dialogs.helper_settings.input_text.pattern_label"
|
||||
)}
|
||||
.helper=${this.hass!.localize(
|
||||
"ui.dialogs.helper_settings.input_text.pattern_helper"
|
||||
"ui.dialogs.helper_settings.input_text.pattern"
|
||||
)}
|
||||
></ha-textfield>
|
||||
`
|
||||
|
@@ -79,7 +79,6 @@ const integrationsWithPanel = {
|
||||
zha: "/config/zha/dashboard",
|
||||
zwave_js: "/config/zwave_js/dashboard",
|
||||
matter: "/config/matter",
|
||||
otbr: "/config/thread",
|
||||
};
|
||||
|
||||
@customElement("ha-integration-card")
|
||||
|
@@ -14,7 +14,6 @@ import { HomeAssistant } from "../../../../../types";
|
||||
import "../../../../../components/ha-alert";
|
||||
import { showPromptDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
|
||||
|
||||
@customElement("matter-config-panel")
|
||||
export class MatterConfigPanel extends LitElement {
|
||||
@@ -33,24 +32,18 @@ export class MatterConfigPanel extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage .narrow=${this.narrow} .hass=${this.hass} header="Matter">
|
||||
${isComponentLoaded(this.hass, "otbr")
|
||||
? html`
|
||||
<a href="/config/thread" slot="toolbar-icon">
|
||||
<mwc-button>Visit Thread Panel</mwc-button>
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
<div class="content">
|
||||
<ha-card header="Matter">
|
||||
<ha-alert alert-type="warning"
|
||||
>Matter is still in the early phase of development, it is not
|
||||
meant to be used in production. This panel is for development
|
||||
only.</ha-alert
|
||||
>
|
||||
<div class="card-content">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<ha-alert alert-type="warning"
|
||||
>Matter is still in the early phase of development, it is not
|
||||
meant to be used in production. This panel is for development
|
||||
only.</ha-alert
|
||||
>
|
||||
|
||||
You can add Matter devices by commissing them if they are not
|
||||
setup yet, or share them from another controller and enter the
|
||||
share code.
|
||||
@@ -206,10 +199,6 @@ export class MatterConfigPanel extends LitElement {
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
ha-alert[alert-type="warning"] {
|
||||
position: relative;
|
||||
top: -16px;
|
||||
}
|
||||
.content {
|
||||
padding: 24px 0 32px;
|
||||
max-width: 600px;
|
||||
@@ -219,9 +208,6 @@ export class MatterConfigPanel extends LitElement {
|
||||
ha-card:first-child {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
a[slot="toolbar-icon"] {
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -1,73 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { threadGetInfo, ThreadInfo } from "../../../../../data/thread";
|
||||
|
||||
@customElement("thread-config-panel")
|
||||
export class ThreadConfigPanel extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@state() private _info?: ThreadInfo;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage .narrow=${this.narrow} .hass=${this.hass} header="Thread">
|
||||
<div class="content">
|
||||
<ha-card header="Thread Border Router">
|
||||
<div class="card-content">
|
||||
${!this._info
|
||||
? html`<ha-circular-progress active></ha-circular-progress>`
|
||||
: html`
|
||||
<table>
|
||||
<tr>
|
||||
<td>URL</td>
|
||||
<td>${this._info.url}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Active Dataset TLVs</td>
|
||||
<td>${this._info.active_dataset_tlvs || "-"}</td>
|
||||
</tr>
|
||||
</table>
|
||||
`}
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
protected override firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
threadGetInfo(this.hass).then((info) => {
|
||||
this._info = info;
|
||||
});
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
.content {
|
||||
padding: 24px 0 32px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
direction: ltr;
|
||||
}
|
||||
ha-card:first-child {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"thread-config-panel": ThreadConfigPanel;
|
||||
}
|
||||
}
|
@@ -50,7 +50,7 @@ export class ZHAManageClusters extends LitElement {
|
||||
|
||||
@state() private _selectedCluster?: Cluster;
|
||||
|
||||
@state() private _currTab: (typeof tabs)[number] = "attributes";
|
||||
@state() private _currTab: typeof tabs[number] = "attributes";
|
||||
|
||||
@state() private _clustersLoaded = false;
|
||||
|
||||
|
@@ -95,7 +95,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
(
|
||||
hasID: boolean,
|
||||
useBluePrint?: boolean,
|
||||
currentMode?: (typeof MODES)[number]
|
||||
currentMode?: typeof MODES[number]
|
||||
) =>
|
||||
[
|
||||
{
|
||||
@@ -528,7 +528,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
// Mode must be one of max modes per schema definition above
|
||||
return this.hass.localize(
|
||||
`ui.panel.config.script.editor.max.${
|
||||
data.mode as (typeof MODES_MAX)[number]
|
||||
data.mode as typeof MODES_MAX[number]
|
||||
}`
|
||||
);
|
||||
default:
|
||||
|
@@ -8,14 +8,11 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { throttle } from "../../../common/util/throttle";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/chart/state-history-charts";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import {
|
||||
HistoryResult,
|
||||
subscribeHistoryStatesTimeWindow,
|
||||
computeHistory,
|
||||
} from "../../../data/history";
|
||||
import { CacheConfig, getRecentWithCache } from "../../../data/cached-history";
|
||||
import { HistoryResult } from "../../../data/history";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { hasConfigOrEntitiesChanged } from "../common/has-changed";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
@@ -45,15 +42,11 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
|
||||
private _names: Record<string, string> = {};
|
||||
|
||||
private _entityIds: string[] = [];
|
||||
private _cacheConfig?: CacheConfig;
|
||||
|
||||
private _hoursToShow = 24;
|
||||
private _fetching = false;
|
||||
|
||||
private _error?: string;
|
||||
|
||||
private _interval?: number;
|
||||
|
||||
private _subscribed?: Promise<(() => Promise<void>) | void>;
|
||||
private _throttleGetStateHistory?: () => void;
|
||||
|
||||
public getCardSize(): number {
|
||||
return this._config?.title
|
||||
@@ -74,81 +67,27 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
? processConfigEntities(config.entities)
|
||||
: [];
|
||||
|
||||
const _entities: string[] = [];
|
||||
|
||||
this._configEntities.forEach((entity) => {
|
||||
this._entityIds.push(entity.entity);
|
||||
_entities.push(entity.entity);
|
||||
if (entity.name) {
|
||||
this._names[entity.entity] = entity.name;
|
||||
}
|
||||
});
|
||||
|
||||
this._hoursToShow = config.hours_to_show || 24;
|
||||
this._throttleGetStateHistory = throttle(() => {
|
||||
this._getStateHistory();
|
||||
}, config.refresh_interval || 10 * 1000);
|
||||
|
||||
this._cacheConfig = {
|
||||
cacheKey: _entities.join(),
|
||||
hoursToShow: config.hours_to_show || 24,
|
||||
};
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
this._subscribeHistoryTimeWindow();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribeHistoryTimeWindow();
|
||||
}
|
||||
|
||||
private _subscribeHistoryTimeWindow() {
|
||||
if (!isComponentLoaded(this.hass!, "history") || this._subscribed) {
|
||||
return;
|
||||
}
|
||||
this._subscribed = subscribeHistoryStatesTimeWindow(
|
||||
this.hass!,
|
||||
(combinedHistory) => {
|
||||
if (!this._subscribed) {
|
||||
// Message came in before we had a chance to unload
|
||||
return;
|
||||
}
|
||||
this._stateHistory = computeHistory(
|
||||
this.hass!,
|
||||
combinedHistory,
|
||||
this.hass!.localize
|
||||
);
|
||||
},
|
||||
this._hoursToShow,
|
||||
this._entityIds
|
||||
).catch((err) => {
|
||||
this._subscribed = undefined;
|
||||
this._error = err;
|
||||
});
|
||||
this._setRedrawTimer();
|
||||
}
|
||||
|
||||
private _redrawGraph() {
|
||||
if (this._stateHistory) {
|
||||
this._stateHistory = { ...this._stateHistory };
|
||||
}
|
||||
}
|
||||
|
||||
private _setRedrawTimer() {
|
||||
// redraw the graph every minute to update the time axis
|
||||
clearInterval(this._interval);
|
||||
this._interval = window.setInterval(() => this._redrawGraph(), 1000 * 60);
|
||||
}
|
||||
|
||||
private _unsubscribeHistoryTimeWindow() {
|
||||
if (!this._subscribed) {
|
||||
return;
|
||||
}
|
||||
clearInterval(this._interval);
|
||||
this._subscribed.then((unsubscribe) => {
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
}
|
||||
this._subscribed = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.has("_stateHistory")) {
|
||||
return true;
|
||||
@@ -161,8 +100,8 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this._hoursToShow ||
|
||||
!this._entityIds.length
|
||||
!this._throttleGetStateHistory ||
|
||||
!this._cacheConfig
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -177,12 +116,13 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
|
||||
if (
|
||||
changedProps.has("_config") &&
|
||||
(!this._subscribed ||
|
||||
oldConfig?.entities !== this._config.entities ||
|
||||
oldConfig?.hours_to_show !== this._hoursToShow)
|
||||
(oldConfig?.entities !== this._config.entities ||
|
||||
oldConfig?.hours_to_show !== this._config.hours_to_show)
|
||||
) {
|
||||
this._unsubscribeHistoryTimeWindow();
|
||||
this._subscribeHistoryTimeWindow();
|
||||
this._throttleGetStateHistory();
|
||||
} else if (changedProps.has("hass")) {
|
||||
// wait for commit of data (we only account for the default setting of 1 sec)
|
||||
setTimeout(this._throttleGetStateHistory, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,10 +131,6 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
return html``;
|
||||
}
|
||||
|
||||
if (this._error) {
|
||||
return html`<div class="errors">${this._error}</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card .header=${this._config.title}>
|
||||
<div
|
||||
@@ -217,6 +153,26 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _getStateHistory(): Promise<void> {
|
||||
if (this._fetching) {
|
||||
return;
|
||||
}
|
||||
this._fetching = true;
|
||||
try {
|
||||
this._stateHistory = {
|
||||
...(await getRecentWithCache(
|
||||
this.hass!,
|
||||
this._configEntities!.map((config) => config.entity),
|
||||
this._cacheConfig!,
|
||||
this.hass!.localize,
|
||||
this.hass!.language
|
||||
)),
|
||||
};
|
||||
} finally {
|
||||
this._fetching = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-card {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { HassEntities } from "home-assistant-js-websocket";
|
||||
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
||||
import { LatLngTuple } from "leaflet";
|
||||
import {
|
||||
css,
|
||||
@@ -12,15 +12,11 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { mdiImageFilterCenterFocus } from "@mdi/js";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isToday } from "date-fns";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import {
|
||||
HistoryStates,
|
||||
subscribeHistoryStatesTimeWindow,
|
||||
} from "../../../data/history";
|
||||
import { fetchRecent } from "../../../data/history";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
@@ -40,7 +36,8 @@ import {
|
||||
formatTimeWeekday,
|
||||
} from "../../../common/datetime/format_time";
|
||||
|
||||
const DEFAULT_HOURS_TO_SHOW = 24;
|
||||
const MINUTE = 60000;
|
||||
|
||||
@customElement("hui-map-card")
|
||||
class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -48,7 +45,8 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public isPanel = false;
|
||||
|
||||
@state() private _stateHistory?: HistoryStates;
|
||||
@state()
|
||||
private _history?: HassEntity[][];
|
||||
|
||||
@state()
|
||||
private _config?: MapCardConfig;
|
||||
@@ -56,16 +54,14 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
@query("ha-map")
|
||||
private _map?: HaMap;
|
||||
|
||||
private _date?: Date;
|
||||
|
||||
private _configEntities?: string[];
|
||||
|
||||
private _colorDict: Record<string, string> = {};
|
||||
|
||||
private _colorIndex = 0;
|
||||
|
||||
private _error?: string;
|
||||
|
||||
private _subscribed?: Promise<(() => Promise<void>) | void>;
|
||||
|
||||
public setConfig(config: MapCardConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Error in card configuration.");
|
||||
@@ -92,6 +88,8 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
? processConfigEntities<EntityConfig>(config.entities)
|
||||
: []
|
||||
).map((entity) => entity.entity);
|
||||
|
||||
this._cleanupHistory();
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
@@ -135,9 +133,6 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
if (this._error) {
|
||||
return html`<div class="error">${this._error}</div>`;
|
||||
}
|
||||
return html`
|
||||
<ha-card id="card" .header=${this._config.title}>
|
||||
<div id="root">
|
||||
@@ -149,7 +144,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
this._configEntities
|
||||
)}
|
||||
.zoom=${this._config.default_zoom ?? 14}
|
||||
.paths=${this._getHistoryPaths(this._config, this._stateHistory)}
|
||||
.paths=${this._getHistoryPaths(this._config, this._history)}
|
||||
.autoFit=${this._config.auto_fit}
|
||||
.darkMode=${this._config.dark_mode}
|
||||
></ha-map>
|
||||
@@ -181,68 +176,23 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (changedProps.has("_stateHistory")) {
|
||||
return true;
|
||||
// Check if any state has changed
|
||||
for (const entity of this._configEntities) {
|
||||
if (oldHass.states[entity] !== this.hass!.states[entity]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated && this._configEntities?.length) {
|
||||
this._subscribeHistoryTimeWindow();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribeHistoryTimeWindow();
|
||||
}
|
||||
|
||||
private _subscribeHistoryTimeWindow() {
|
||||
if (!isComponentLoaded(this.hass!, "history") || this._subscribed) {
|
||||
return;
|
||||
}
|
||||
this._subscribed = subscribeHistoryStatesTimeWindow(
|
||||
this.hass!,
|
||||
(combinedHistory) => {
|
||||
if (!this._subscribed) {
|
||||
// Message came in before we had a chance to unload
|
||||
return;
|
||||
}
|
||||
this._stateHistory = combinedHistory;
|
||||
},
|
||||
this._config!.hours_to_show! || DEFAULT_HOURS_TO_SHOW,
|
||||
this._configEntities!,
|
||||
false,
|
||||
false
|
||||
).catch((err) => {
|
||||
this._subscribed = undefined;
|
||||
this._error = err;
|
||||
});
|
||||
}
|
||||
|
||||
private _unsubscribeHistoryTimeWindow() {
|
||||
if (!this._subscribed) {
|
||||
return;
|
||||
}
|
||||
this._subscribed.then((unsubscribe) => {
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
}
|
||||
this._subscribed = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
if (this._configEntities?.length) {
|
||||
if (!this._subscribed || changedProps.has("_config")) {
|
||||
this._unsubscribeHistoryTimeWindow();
|
||||
this._subscribeHistoryTimeWindow();
|
||||
if (this._config?.hours_to_show && this._configEntities?.length) {
|
||||
if (changedProps.has("_config")) {
|
||||
this._getHistory();
|
||||
} else if (Date.now() - this._date!.getTime() >= MINUTE) {
|
||||
this._getHistory();
|
||||
}
|
||||
} else {
|
||||
this._unsubscribeHistoryTimeWindow();
|
||||
}
|
||||
if (changedProps.has("_config")) {
|
||||
this._computePadding();
|
||||
@@ -322,44 +272,46 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
private _getHistoryPaths = memoizeOne(
|
||||
(
|
||||
config: MapCardConfig,
|
||||
history?: HistoryStates
|
||||
history?: HassEntity[][]
|
||||
): HaMapPaths[] | undefined => {
|
||||
if (!history) {
|
||||
if (!config.hours_to_show || !history) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const paths: HaMapPaths[] = [];
|
||||
|
||||
for (const entityId of Object.keys(history)) {
|
||||
const entityStates = history[entityId];
|
||||
if (!entityStates?.length) {
|
||||
for (const entityStates of history) {
|
||||
if (entityStates?.length <= 1) {
|
||||
continue;
|
||||
}
|
||||
// filter location data from states and remove all invalid locations
|
||||
const points: HaMapPathPoint[] = [];
|
||||
for (const entityState of entityStates) {
|
||||
const latitude = entityState.a.latitude;
|
||||
const longitude = entityState.a.longitude;
|
||||
if (!latitude || !longitude) {
|
||||
continue;
|
||||
}
|
||||
const p = {} as HaMapPathPoint;
|
||||
p.point = [latitude, longitude] as LatLngTuple;
|
||||
const t = new Date(entityState.lu * 1000);
|
||||
if (config.hours_to_show! || DEFAULT_HOURS_TO_SHOW > 144) {
|
||||
// if showing > 6 days in the history trail, show the full
|
||||
// date and time
|
||||
p.tooltip = formatDateTime(t, this.hass.locale);
|
||||
} else if (isToday(t)) {
|
||||
p.tooltip = formatTime(t, this.hass.locale);
|
||||
} else {
|
||||
p.tooltip = formatTimeWeekday(t, this.hass.locale);
|
||||
}
|
||||
points.push(p);
|
||||
}
|
||||
const points = entityStates.reduce(
|
||||
(accumulator: HaMapPathPoint[], entityState) => {
|
||||
const latitude = entityState.attributes.latitude;
|
||||
const longitude = entityState.attributes.longitude;
|
||||
if (latitude && longitude) {
|
||||
const p = {} as HaMapPathPoint;
|
||||
p.point = [latitude, longitude] as LatLngTuple;
|
||||
const t = new Date(entityState.last_updated);
|
||||
if (config.hours_to_show! > 144) {
|
||||
// if showing > 6 days in the history trail, show the full
|
||||
// date and time
|
||||
p.tooltip = formatDateTime(t, this.hass.locale);
|
||||
} else if (isToday(t)) {
|
||||
p.tooltip = formatTime(t, this.hass.locale);
|
||||
} else {
|
||||
p.tooltip = formatTimeWeekday(t, this.hass.locale);
|
||||
}
|
||||
accumulator.push(p);
|
||||
}
|
||||
return accumulator;
|
||||
},
|
||||
[]
|
||||
) as HaMapPathPoint[];
|
||||
|
||||
paths.push({
|
||||
points,
|
||||
color: this._getColor(entityId),
|
||||
color: this._getColor(entityStates[0].entity_id),
|
||||
gradualOpacity: 0.8,
|
||||
});
|
||||
}
|
||||
@@ -367,6 +319,58 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
);
|
||||
|
||||
private async _getHistory(): Promise<void> {
|
||||
this._date = new Date();
|
||||
|
||||
if (!this._configEntities) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entityIds = this._configEntities!.join(",");
|
||||
const endTime = new Date();
|
||||
const startTime = new Date();
|
||||
startTime.setHours(endTime.getHours() - this._config!.hours_to_show!);
|
||||
const skipInitialState = false;
|
||||
const significantChangesOnly = false;
|
||||
const minimalResponse = false;
|
||||
|
||||
const stateHistory = await fetchRecent(
|
||||
this.hass,
|
||||
entityIds,
|
||||
startTime,
|
||||
endTime,
|
||||
skipInitialState,
|
||||
significantChangesOnly,
|
||||
minimalResponse
|
||||
);
|
||||
|
||||
if (stateHistory.length < 1) {
|
||||
return;
|
||||
}
|
||||
this._history = stateHistory;
|
||||
}
|
||||
|
||||
private _cleanupHistory() {
|
||||
if (!this._history) {
|
||||
return;
|
||||
}
|
||||
if (this._config!.hours_to_show! <= 0) {
|
||||
this._history = undefined;
|
||||
} else {
|
||||
// remove unused entities
|
||||
this._history = this._history!.reduce(
|
||||
(accumulator: HassEntity[][], entityStates) => {
|
||||
const entityId = entityStates[0].entity_id;
|
||||
if (this._configEntities?.includes(entityId)) {
|
||||
accumulator.push(entityStates);
|
||||
}
|
||||
return accumulator;
|
||||
},
|
||||
[]
|
||||
) as HassEntity[][];
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-card {
|
||||
|
@@ -1,16 +1,8 @@
|
||||
import { memoize } from "@fullcalendar/common";
|
||||
import { Ripple } from "@material/mwc-ripple";
|
||||
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
|
||||
import { mdiExclamationThick, mdiHelp } from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
queryAsync,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { hsv2rgb, rgb2hsv } from "../../../common/color/convert-color";
|
||||
@@ -113,8 +105,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
handleAction(this, this.hass!, this._config!, ev.detail.action!);
|
||||
}
|
||||
|
||||
private _handleIconAction(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
private _handleIconAction() {
|
||||
const config = {
|
||||
entity: this._config!.entity,
|
||||
tap_action: this._config!.icon_tap_action,
|
||||
@@ -228,32 +219,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
return stateDisplay;
|
||||
}
|
||||
|
||||
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
|
||||
|
||||
@state() private _shouldRenderRipple = false;
|
||||
|
||||
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
|
||||
this._shouldRenderRipple = true;
|
||||
return this._ripple;
|
||||
});
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private handleRippleActivate(evt?: Event) {
|
||||
this._rippleHandlers.startPress(evt);
|
||||
}
|
||||
|
||||
private handleRippleDeactivate() {
|
||||
this._rippleHandlers.endPress();
|
||||
}
|
||||
|
||||
private handleRippleMouseEnter() {
|
||||
this._rippleHandlers.startHover();
|
||||
}
|
||||
|
||||
private handleRippleMouseLeave() {
|
||||
this._rippleHandlers.endHover();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config || !this.hass) {
|
||||
return html``;
|
||||
@@ -309,7 +274,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
|
||||
return html`
|
||||
<ha-card style=${styleMap(style)}>
|
||||
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : null}
|
||||
<div class="tile">
|
||||
<div
|
||||
class="icon-container"
|
||||
@@ -349,17 +313,10 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
class="info"
|
||||
.primary=${name}
|
||||
.secondary=${stateDisplay}
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler()}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@mousedown=${this.handleRippleActivate}
|
||||
@mouseup=${this.handleRippleDeactivate}
|
||||
@mouseenter=${this.handleRippleMouseEnter}
|
||||
@mouseleave=${this.handleRippleMouseLeave}
|
||||
@touchstart=${this.handleRippleActivate}
|
||||
@touchend=${this.handleRippleDeactivate}
|
||||
@touchcancel=${this.handleRippleDeactivate}
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler()}
|
||||
></ha-tile-info>
|
||||
</div>
|
||||
${supportedFeatures?.length
|
||||
@@ -408,18 +365,11 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
return css`
|
||||
:host {
|
||||
--tile-color: rgb(var(--rgb-state-inactive-color));
|
||||
--tile-tap-padding: 6px;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
ha-card:has(ha-tile-info:focus-visible) {
|
||||
border-color: var(--tile-color);
|
||||
box-shadow: 0 0 0 1px var(--tile-color);
|
||||
}
|
||||
ha-card {
|
||||
--mdc-ripple-color: var(--tile-color);
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
// For safari overflow hidden
|
||||
z-index: 0;
|
||||
}
|
||||
ha-card.disabled {
|
||||
--tile-color: rgb(var(--rgb-disabled-color));
|
||||
@@ -431,16 +381,18 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
outline: none;
|
||||
}
|
||||
.tile {
|
||||
padding: calc(12px - var(--tile-tap-padding));
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.icon-container {
|
||||
position: relative;
|
||||
padding: var(--tile-tap-padding);
|
||||
flex: none;
|
||||
margin-right: 12px;
|
||||
margin-inline-start: 12px;
|
||||
margin-inline-end: initial;
|
||||
margin-right: calc(12px - 2 * var(--tile-tap-padding));
|
||||
margin-inline-end: calc(12px - 2 * var(--tile-tap-padding));
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
transition: transform 180ms ease-in-out;
|
||||
}
|
||||
@@ -449,8 +401,8 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
.icon-container .badge {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
right: -3px;
|
||||
top: calc(-3px + var(--tile-tap-padding));
|
||||
right: calc(-3px + var(--tile-tap-padding));
|
||||
}
|
||||
.icon-container[role="button"]:focus-visible,
|
||||
.icon-container[role="button"]:active {
|
||||
@@ -458,12 +410,27 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
.info {
|
||||
position: relative;
|
||||
padding: 12px;
|
||||
padding: var(--tile-tap-padding);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 40px;
|
||||
transition: background-color 180ms ease-in-out;
|
||||
}
|
||||
.info::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: calc(var(--ha-card-border-radius, 10px) - 2px);
|
||||
background-color: transparent;
|
||||
opacity: 0.1;
|
||||
transition: background-color ease-in-out 180ms;
|
||||
}
|
||||
.info:focus-visible::before {
|
||||
background-color: var(--tile-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -161,7 +161,7 @@ export const computeCards = (
|
||||
renderFooterEntities &&
|
||||
(domain === "scene" || domain === "script")
|
||||
) {
|
||||
const conf: (typeof footerEntities)[0] = {
|
||||
const conf: typeof footerEntities[0] = {
|
||||
entity: entityId,
|
||||
show_icon: true,
|
||||
show_name: true,
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { strokeWidth } from "../../../../data/graph";
|
||||
import { EntityHistoryState } from "../../../../data/history";
|
||||
|
||||
const average = (items: any[]): number =>
|
||||
items.reduce((sum, entry) => sum + parseFloat(entry.state), 0) / items.length;
|
||||
@@ -106,25 +105,3 @@ export const coordinates = (
|
||||
|
||||
return calcPoints(history, hours, width, detail, min, max);
|
||||
};
|
||||
|
||||
interface NumericEntityHistoryState {
|
||||
state: number;
|
||||
last_changed: number;
|
||||
}
|
||||
|
||||
export const coordinatesMinimalResponseCompressedState = (
|
||||
history: EntityHistoryState[],
|
||||
hours: number,
|
||||
width: number,
|
||||
detail: number,
|
||||
limits?: { min?: number; max?: number }
|
||||
): number[][] | undefined => {
|
||||
const numericHistory: NumericEntityHistoryState[] = history.map((item) => ({
|
||||
state: Number(item.s),
|
||||
// With minimal response and compressed state, we don't have last_changed,
|
||||
// so we use last_updated since its always the same as last_changed since
|
||||
// we already filtered out states that are the same.
|
||||
last_changed: item.lu * 1000,
|
||||
}));
|
||||
return coordinates(numericHistory, hours, width, detail, limits);
|
||||
};
|
||||
|
@@ -16,4 +16,4 @@ export const TIMESTAMP_RENDERING_FORMATS = [
|
||||
] as const;
|
||||
|
||||
export type TimestampRenderingFormat =
|
||||
(typeof TIMESTAMP_RENDERING_FORMATS)[number];
|
||||
typeof TIMESTAMP_RENDERING_FORMATS[number];
|
||||
|
@@ -38,7 +38,7 @@ const cardConfigStruct = assign(
|
||||
|
||||
const stat_types = ["mean", "min", "max", "change"] as const;
|
||||
|
||||
const statTypeMap: Record<(typeof stat_types)[number], StatisticType> = {
|
||||
const statTypeMap: Record<typeof stat_types[number], StatisticType> = {
|
||||
mean: "mean",
|
||||
min: "min",
|
||||
max: "max",
|
||||
|
@@ -102,7 +102,6 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow {
|
||||
}
|
||||
ha-select {
|
||||
width: 100%;
|
||||
--ha-select-min-width: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -9,18 +9,18 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { subscribeHistoryStatesTimeWindow } from "../../../data/history";
|
||||
import { fetchRecent } from "../../../data/history";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { coordinatesMinimalResponseCompressedState } from "../common/graph/coordinates";
|
||||
import { coordinates } from "../common/graph/coordinates";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-graph-base";
|
||||
import { LovelaceHeaderFooter, LovelaceHeaderFooterEditor } from "../types";
|
||||
import { GraphHeaderFooterConfig } from "./types";
|
||||
|
||||
const MINUTE = 60000;
|
||||
const HOUR = 60 * MINUTE;
|
||||
const HOUR = MINUTE * 60;
|
||||
const includeDomains = ["counter", "input_number", "number", "sensor"];
|
||||
|
||||
@customElement("hui-graph-header-footer")
|
||||
@@ -66,11 +66,11 @@ export class HuiGraphHeaderFooter
|
||||
|
||||
@state() private _coordinates?: number[][];
|
||||
|
||||
private _error?: string;
|
||||
private _date?: Date;
|
||||
|
||||
private _interval?: number;
|
||||
private _stateHistory?: HassEntity[];
|
||||
|
||||
private _subscribed?: Promise<(() => Promise<void>) | void>;
|
||||
private _fetching = false;
|
||||
|
||||
public getCardSize(): number {
|
||||
return 3;
|
||||
@@ -104,10 +104,6 @@ export class HuiGraphHeaderFooter
|
||||
return html``;
|
||||
}
|
||||
|
||||
if (this._error) {
|
||||
return html`<div class="errors">${this._error}</div>`;
|
||||
}
|
||||
|
||||
if (!this._coordinates) {
|
||||
return html`
|
||||
<div class="container">
|
||||
@@ -129,91 +125,89 @@ export class HuiGraphHeaderFooter
|
||||
`;
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
this._subscribeHistoryTimeWindow();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribeHistoryTimeWindow();
|
||||
}
|
||||
|
||||
private _subscribeHistoryTimeWindow() {
|
||||
if (!isComponentLoaded(this.hass!, "history") || this._subscribed) {
|
||||
return;
|
||||
}
|
||||
this._subscribed = subscribeHistoryStatesTimeWindow(
|
||||
this.hass!,
|
||||
(combinedHistory) => {
|
||||
if (!this._subscribed) {
|
||||
// Message came in before we had a chance to unload
|
||||
return;
|
||||
}
|
||||
this._coordinates =
|
||||
coordinatesMinimalResponseCompressedState(
|
||||
combinedHistory[this._config!.entity],
|
||||
this._config!.hours_to_show!,
|
||||
500,
|
||||
this._config!.detail!,
|
||||
this._config!.limits
|
||||
) || [];
|
||||
},
|
||||
this._config!.hours_to_show!,
|
||||
[this._config!.entity]
|
||||
).catch((err) => {
|
||||
this._subscribed = undefined;
|
||||
this._error = err;
|
||||
});
|
||||
this._setRedrawTimer();
|
||||
}
|
||||
|
||||
private _redrawGraph() {
|
||||
if (this._coordinates) {
|
||||
this._coordinates = [...this._coordinates];
|
||||
}
|
||||
}
|
||||
|
||||
private _setRedrawTimer() {
|
||||
// redraw the graph every minute to update the time axis
|
||||
clearInterval(this._interval);
|
||||
this._interval = window.setInterval(
|
||||
() => this._redrawGraph(),
|
||||
this._config!.hours_to_show! > 24 ? HOUR : MINUTE
|
||||
);
|
||||
}
|
||||
|
||||
private _unsubscribeHistoryTimeWindow() {
|
||||
clearInterval(this._interval);
|
||||
if (!this._subscribed) {
|
||||
return;
|
||||
}
|
||||
this._subscribed.then((unsubscribe) => {
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
}
|
||||
this._subscribed = undefined;
|
||||
});
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
return hasConfigOrEntityChanged(this, changedProps);
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (!this._config || !this.hass || !changedProps.has("_config")) {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
(this._fetching && !changedProps.has("_config"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldConfig = changedProps.get("_config") as GraphHeaderFooterConfig;
|
||||
if (
|
||||
!oldConfig ||
|
||||
!this._subscribed ||
|
||||
oldConfig.entity !== this._config.entity
|
||||
) {
|
||||
this._unsubscribeHistoryTimeWindow();
|
||||
this._subscribeHistoryTimeWindow();
|
||||
if (changedProps.has("_config")) {
|
||||
const oldConfig = changedProps.get("_config") as GraphHeaderFooterConfig;
|
||||
if (!oldConfig || oldConfig.entity !== this._config.entity) {
|
||||
this._stateHistory = [];
|
||||
}
|
||||
|
||||
this._getCoordinates();
|
||||
} else if (Date.now() - this._date!.getTime() >= MINUTE) {
|
||||
this._getCoordinates();
|
||||
}
|
||||
}
|
||||
|
||||
private async _getCoordinates(): Promise<void> {
|
||||
this._fetching = true;
|
||||
const endTime = new Date();
|
||||
const startTime =
|
||||
!this._date || !this._stateHistory?.length
|
||||
? new Date(
|
||||
new Date().setHours(
|
||||
endTime.getHours() - this._config!.hours_to_show!
|
||||
)
|
||||
)
|
||||
: this._date;
|
||||
|
||||
if (this._stateHistory!.length) {
|
||||
const inHoursToShow: HassEntity[] = [];
|
||||
const outHoursToShow: HassEntity[] = [];
|
||||
// Split into inside and outside of "hours to show".
|
||||
this._stateHistory!.forEach((entity) =>
|
||||
(endTime.getTime() - new Date(entity.last_changed).getTime() <=
|
||||
this._config!.hours_to_show! * HOUR
|
||||
? inHoursToShow
|
||||
: outHoursToShow
|
||||
).push(entity)
|
||||
);
|
||||
|
||||
if (outHoursToShow.length) {
|
||||
// If we have values that are now outside of "hours to show", re-add the last entry. This could e.g. be
|
||||
// the "initial state" from the history backend. Without it, it would look like there is no history data
|
||||
// at the start at all in the database = graph would start suddenly instead of on the left side of the card.
|
||||
inHoursToShow.push(outHoursToShow[outHoursToShow.length - 1]);
|
||||
}
|
||||
this._stateHistory = inHoursToShow;
|
||||
}
|
||||
|
||||
const stateHistory = await fetchRecent(
|
||||
this.hass!,
|
||||
this._config!.entity,
|
||||
startTime,
|
||||
endTime,
|
||||
Boolean(this._stateHistory!.length)
|
||||
);
|
||||
|
||||
if (stateHistory.length && stateHistory[0].length) {
|
||||
this._stateHistory!.push(...stateHistory[0]);
|
||||
}
|
||||
|
||||
this._coordinates =
|
||||
coordinates(
|
||||
this._stateHistory,
|
||||
this._config!.hours_to_show!,
|
||||
500,
|
||||
this._config!.detail!,
|
||||
this._config!.limits
|
||||
) || [];
|
||||
|
||||
this._date = endTime;
|
||||
this._fetching = false;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-circular-progress {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { undoDepth } from "@codemirror/commands";
|
||||
import { undoDepth } from "@codemirror/history";
|
||||
import "@material/mwc-button";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
|
@@ -3,13 +3,13 @@ import "@material/mwc-list/mwc-list-item";
|
||||
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
|
||||
import {
|
||||
mdiCodeBraces,
|
||||
mdiCommentProcessingOutline,
|
||||
mdiDotsVertical,
|
||||
mdiFileMultiple,
|
||||
mdiFormatListBulletedTriangle,
|
||||
mdiHelp,
|
||||
mdiHelpCircle,
|
||||
mdiMagnify,
|
||||
mdiMicrophone,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
mdiRefresh,
|
||||
@@ -302,9 +302,9 @@ class HUIRoot extends LitElement {
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.menu.assist"
|
||||
"ui.panel.lovelace.menu.start_conversation"
|
||||
)}
|
||||
.path=${mdiCommentProcessingOutline}
|
||||
.path=${mdiMicrophone}
|
||||
@click=${this._showVoiceCommandDialog}
|
||||
></ha-icon-button>
|
||||
`
|
||||
@@ -324,7 +324,7 @@ class HUIRoot extends LitElement {
|
||||
? html`
|
||||
<mwc-list-item
|
||||
graphic="icon"
|
||||
@request-selected=${this._handleShowQuickBar}
|
||||
@request-selected=${this._showQuickBar}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.menu.search"
|
||||
@@ -343,15 +343,15 @@ class HUIRoot extends LitElement {
|
||||
<mwc-list-item
|
||||
graphic="icon"
|
||||
@request-selected=${this
|
||||
._handleShowVoiceCommandDialog}
|
||||
._showVoiceCommandDialog}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.menu.assist"
|
||||
"ui.panel.lovelace.menu.start_conversation"
|
||||
)}
|
||||
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiCommentProcessingOutline}
|
||||
.path=${mdiMicrophone}
|
||||
></ha-svg-icon>
|
||||
</mwc-list-item>
|
||||
`
|
||||
@@ -711,13 +711,6 @@ class HUIRoot extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleShowQuickBar(ev: CustomEvent<RequestSelectedDetail>): void {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
this._showQuickBar();
|
||||
}
|
||||
|
||||
private _showQuickBar(): void {
|
||||
showQuickBar(this, {
|
||||
commandMode: false,
|
||||
@@ -769,15 +762,6 @@ class HUIRoot extends LitElement {
|
||||
navigate(`${this.route?.prefix}/hass-unused-entities`);
|
||||
}
|
||||
|
||||
private _handleShowVoiceCommandDialog(
|
||||
ev: CustomEvent<RequestSelectedDetail>
|
||||
): void {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
this._showVoiceCommandDialog();
|
||||
}
|
||||
|
||||
private _showVoiceCommandDialog(): void {
|
||||
showVoiceCommandDialog(this);
|
||||
}
|
||||
|
@@ -18,7 +18,7 @@ export const VACUUM_COMMANDS = [
|
||||
"return_home",
|
||||
] as const;
|
||||
|
||||
export type VacuumCommand = (typeof VACUUM_COMMANDS)[number];
|
||||
export type VacuumCommand = typeof VACUUM_COMMANDS[number];
|
||||
|
||||
export interface VacuumCommandsTileFeatureConfig {
|
||||
type: "vacuum-commands";
|
||||
|
@@ -1,29 +1,25 @@
|
||||
import { indentLess, indentMore } from "@codemirror/commands";
|
||||
import {
|
||||
HighlightStyle,
|
||||
StreamLanguage,
|
||||
syntaxHighlighting,
|
||||
} from "@codemirror/language";
|
||||
import { HighlightStyle, tags } from "@codemirror/highlight";
|
||||
import { jinja2 } from "@codemirror/legacy-modes/mode/jinja2";
|
||||
import { yaml } from "@codemirror/legacy-modes/mode/yaml";
|
||||
import { Compartment } from "@codemirror/state";
|
||||
import { StreamLanguage } from "@codemirror/stream-parser";
|
||||
import { EditorView, KeyBinding } from "@codemirror/view";
|
||||
import { tags } from "@lezer/highlight";
|
||||
|
||||
export { autocompletion } from "@codemirror/autocomplete";
|
||||
export { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
||||
export { highlightingFor } from "@codemirror/language";
|
||||
export { defaultKeymap } from "@codemirror/commands";
|
||||
export { lineNumbers } from "@codemirror/gutter";
|
||||
export { HighlightStyle, tags } from "@codemirror/highlight";
|
||||
export { history, historyKeymap } from "@codemirror/history";
|
||||
export { rectangularSelection } from "@codemirror/rectangular-selection";
|
||||
export { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
|
||||
export { EditorState } from "@codemirror/state";
|
||||
export { EditorState, Prec } from "@codemirror/state";
|
||||
export { autocompletion } from "@codemirror/autocomplete";
|
||||
export {
|
||||
drawSelection,
|
||||
EditorView,
|
||||
highlightActiveLine,
|
||||
keymap,
|
||||
lineNumbers,
|
||||
rectangularSelection,
|
||||
} from "@codemirror/view";
|
||||
export { tags } from "@lezer/highlight";
|
||||
|
||||
export const langs = {
|
||||
jinja2: StreamLanguage.define(jinja2),
|
||||
@@ -41,7 +37,7 @@ export const tabKeyBindings: KeyBinding[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const haTheme = EditorView.theme({
|
||||
export const theme = EditorView.theme({
|
||||
"&": {
|
||||
color: "var(--primary-text-color)",
|
||||
backgroundColor:
|
||||
@@ -190,7 +186,7 @@ export const haTheme = EditorView.theme({
|
||||
".cm-gutterElement.lineNumber": { color: "inherit" },
|
||||
});
|
||||
|
||||
const haHighlightStyle = HighlightStyle.define([
|
||||
export const highlightStyle = HighlightStyle.define([
|
||||
{ tag: tags.keyword, color: "var(--codemirror-keyword, #6262FF)" },
|
||||
{
|
||||
tag: [
|
||||
@@ -263,5 +259,3 @@ const haHighlightStyle = HighlightStyle.define([
|
||||
{ tag: tags.inserted, color: "var(--codemirror-string2, #07a)" },
|
||||
{ tag: tags.invalid, color: "var(--error-color)" },
|
||||
]);
|
||||
|
||||
export const haSyntaxHighlighting = syntaxHighlighting(haHighlightStyle);
|
||||
|
@@ -172,9 +172,7 @@ documentContainer.innerHTML = `<custom-style>
|
||||
--rgb-state-person-home-color: var(--rgb-green-color);
|
||||
--rgb-state-person-not-home-color: var(--rgb-grey-color);
|
||||
--rgb-state-person-zone-color: var(--rgb-blue-color);
|
||||
--rgb-state-plant-color: var(--rgb-red-color);
|
||||
--rgb-state-remote-color: var(--rgb-amber-color);
|
||||
--rgb-state-schedule-color: var(--rgb-amber-color);
|
||||
--rgb-state-script-color: var(--rgb-amber-color);
|
||||
--rgb-state-sensor-battery-high-color: var(--rgb-green-color);
|
||||
--rgb-state-sensor-battery-low-color: var(--rgb-red-color);
|
||||
|
@@ -2,10 +2,14 @@
|
||||
import { expose } from "comlink";
|
||||
import { marked } from "marked";
|
||||
import "proxy-polyfill";
|
||||
import { filterXSS, getDefaultWhiteList, IWhiteList } from "xss";
|
||||
import { filterXSS, getDefaultWhiteList } from "xss";
|
||||
|
||||
let whiteListNormal: IWhiteList | undefined;
|
||||
let whiteListSvg: IWhiteList | undefined;
|
||||
interface WhiteList {
|
||||
[tag: string]: string[];
|
||||
}
|
||||
|
||||
let whiteListNormal: WhiteList | undefined;
|
||||
let whiteListSvg: WhiteList | undefined;
|
||||
|
||||
// Override the default `onTagAttr` behavior to only render
|
||||
// our markdown checkboxes.
|
||||
@@ -39,7 +43,7 @@ const renderMarkdown = (
|
||||
): string => {
|
||||
if (!whiteListNormal) {
|
||||
whiteListNormal = {
|
||||
...getDefaultWhiteList(),
|
||||
...(getDefaultWhiteList() as WhiteList),
|
||||
input: ["type", "disabled", "checked"],
|
||||
"ha-icon": ["icon"],
|
||||
"ha-svg-icon": ["path"],
|
||||
@@ -47,7 +51,7 @@ const renderMarkdown = (
|
||||
};
|
||||
}
|
||||
|
||||
let whiteList: IWhiteList | undefined;
|
||||
let whiteList: WhiteList | undefined;
|
||||
|
||||
if (hassOptions.allowSvg) {
|
||||
if (!whiteListSvg) {
|
||||
|
@@ -2,7 +2,6 @@ import type { PropertyValues } from "lit";
|
||||
import tinykeys from "tinykeys";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import { HaSelect } from "../components/ha-select";
|
||||
import {
|
||||
QuickBarParams,
|
||||
showQuickBar,
|
||||
@@ -134,17 +133,17 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
}
|
||||
|
||||
private _canOverrideAlphanumericInput(e: KeyboardEvent) {
|
||||
const el = e.composedPath()[0];
|
||||
const el = e.composedPath()[0] as any;
|
||||
|
||||
if (el instanceof HTMLTextAreaElement) {
|
||||
if (el.tagName === "TEXTAREA") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (el instanceof Element && el.parentElement instanceof HaSelect) {
|
||||
if (el.parentElement.tagName === "HA-SELECT") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(el instanceof HTMLInputElement)) {
|
||||
if (el.tagName !== "INPUT") {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@@ -803,15 +803,13 @@
|
||||
"nothing_found": "Nothing found!"
|
||||
},
|
||||
"voice_command": {
|
||||
"title": "Assistant",
|
||||
"did_not_hear": "Home Assistant did not hear anything",
|
||||
"did_not_understand": "Didn't quite get that",
|
||||
"found": "I found the following for you:",
|
||||
"error": "Oops, an error has occurred",
|
||||
"how_can_i_help": "How can I assist?",
|
||||
"input_label": "Enter a request",
|
||||
"send_text": "Send text",
|
||||
"start_listening": "Start listening"
|
||||
"how_can_i_help": "How can I help?",
|
||||
"label": "Type a question and press 'Enter'",
|
||||
"label_voice": "Type and press 'Enter' or tap the microphone to speak"
|
||||
},
|
||||
"generic": {
|
||||
"cancel": "Cancel",
|
||||
@@ -1035,8 +1033,7 @@
|
||||
"mode": "Display mode",
|
||||
"text": "Text",
|
||||
"password": "Password",
|
||||
"pattern_label": "Regex pattern",
|
||||
"pattern_helper": "Used for client-side validation"
|
||||
"pattern": "Regex pattern for client-side validation"
|
||||
},
|
||||
"input_number": {
|
||||
"min": "Minimum value",
|
||||
@@ -1636,6 +1633,66 @@
|
||||
"device_consumption_energy": "Device consumption energy (kWh)",
|
||||
"selected_stat_intro": "Select the entity that represents the device energy usage."
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"issues": {
|
||||
"entity_not_defined": {
|
||||
"title": "Entity not defined",
|
||||
"description": "Check the integration or your configuration that provides:"
|
||||
},
|
||||
"recorder_untracked": {
|
||||
"title": "Entity not tracked",
|
||||
"description": "The recorder has been configured to exclude these configured entities:"
|
||||
},
|
||||
"entity_unavailable": {
|
||||
"title": "Entity unavailable",
|
||||
"description": "The state of these configured entities are currently not available:"
|
||||
},
|
||||
"entity_state_non_numeric": {
|
||||
"title": "Entity has non-numeric state",
|
||||
"description": "The following entities have a state that cannot be parsed as a number:"
|
||||
},
|
||||
"entity_negative_state": {
|
||||
"title": "Entity has a negative state",
|
||||
"description": "The following entities have a negative state while a positive state is expected:"
|
||||
},
|
||||
"entity_unexpected_unit_energy": {
|
||||
"title": "Unexpected unit of measurement",
|
||||
"description": "The following entities do not have the expected units of measurement 'Wh', 'kWh', 'MWh' or 'GJ':"
|
||||
},
|
||||
"entity_unexpected_unit_gas": {
|
||||
"title": "Unexpected unit of measurement",
|
||||
"description": "The following entities do not have the expected units of measurement 'Wh', 'kWh', 'MWh' or 'GJ' for an energy sensor or 'm³' or 'ft³' for a gas sensor:"
|
||||
},
|
||||
"entity_unexpected_unit_water": {
|
||||
"title": "Unexpected unit of measurement",
|
||||
"description": "The following entities do not have the expected units of measurement 'm³' or 'ft³' for a water sensor:"
|
||||
},
|
||||
"entity_unexpected_unit_energy_price": {
|
||||
"title": "Unexpected unit of measurement",
|
||||
"description": "The following entities do not have the expected units of measurement ''{currency}/kWh'', ''{currency}/Wh'', ''{currency}/MWh'' or ''{currency}/GJ'':"
|
||||
},
|
||||
"entity_unexpected_unit_gas_price": {
|
||||
"title": "Unexpected unit of measurement",
|
||||
"description": "The following entities do not have the expected units of measurement ''{currency}/kWh'', ''{currency}/Wh'', ''{currency}/MWh'', ''{currency}/GJ'', ''{currency}/m³'' or ''{currency}/ft³'':"
|
||||
},
|
||||
"entity_unexpected_unit_water_price": {
|
||||
"title": "Unexpected unit of measurement",
|
||||
"description": "The following entities do not have the expected units of measurement ''{currency}/m³'' or ''{currency}/ft³'':"
|
||||
},
|
||||
"entity_unexpected_state_class": {
|
||||
"title": "Unexpected state class",
|
||||
"description": "The following entities do not have the expected state class:"
|
||||
},
|
||||
"entity_unexpected_device_class": {
|
||||
"title": "Unexpected device class",
|
||||
"description": "The following entities do not have the expected device class:"
|
||||
},
|
||||
"entity_state_class_measurement_no_last_reset": {
|
||||
"title": "Last reset missing",
|
||||
"description": "The following entities have state class 'measurement' but 'last_reset' is missing:"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"helpers": {
|
||||
@@ -2683,10 +2740,6 @@
|
||||
"exposed": "{selected} exposed",
|
||||
"not_exposed": "{selected} not exposed",
|
||||
"manage_aliases": "Manage aliases",
|
||||
"add_aliases": "Add aliases",
|
||||
"no_aliases": "No aliases",
|
||||
"aliases_not_available": "Aliases not available",
|
||||
"aliases_not_available_learn_more": "Learn more",
|
||||
"sync_to_google": "Synchronizing changes to Google.",
|
||||
"sync_entities": "Synchronize entities",
|
||||
"sync_entities_error": "Failed to sync entities:",
|
||||
@@ -3858,7 +3911,7 @@
|
||||
"configure_ui": "Edit Dashboard",
|
||||
"help": "Help",
|
||||
"search": "Search",
|
||||
"assist": "Assist",
|
||||
"start_conversation": "Start conversation",
|
||||
"reload_resources": "Reload resources",
|
||||
"exit_edit_mode": "Done",
|
||||
"close": "Close"
|
||||
|
Reference in New Issue
Block a user