mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-16 04:39:28 +00:00
Compare commits
2 Commits
int2
...
20221230.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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)) {
|
||||
|
@@ -71,6 +71,7 @@ class HaDemo extends HomeAssistantAppEl {
|
||||
entity_category: null,
|
||||
has_entity_name: false,
|
||||
unique_id: "co2_intensity",
|
||||
aliases: [],
|
||||
},
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
@@ -86,6 +87,7 @@ class HaDemo extends HomeAssistantAppEl {
|
||||
entity_category: null,
|
||||
has_entity_name: false,
|
||||
unique_id: "grid_fossil_fuel_percentage",
|
||||
aliases: [],
|
||||
},
|
||||
]);
|
||||
|
||||
|
@@ -197,6 +197,7 @@ const createEntityRegistryEntries = (
|
||||
platform: "updater",
|
||||
has_entity_name: false,
|
||||
unique_id: "updater",
|
||||
aliases: [],
|
||||
},
|
||||
];
|
||||
|
||||
|
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 = "20221230.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",
|
||||
|
@@ -157,7 +157,7 @@ export const CURRENCIES = [
|
||||
"XPF",
|
||||
"YER",
|
||||
"ZAR",
|
||||
"ZMW",
|
||||
"ZMK",
|
||||
"ZWL",
|
||||
];
|
||||
|
||||
|
@@ -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 || [];
|
||||
|
@@ -22,7 +22,6 @@ import {
|
||||
isNumericState,
|
||||
} from "../../common/number/format_number";
|
||||
import { isUnavailableState, UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { timerTimeRemaining } from "../../data/timer";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-label-badge";
|
||||
@@ -35,9 +34,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) => {
|
||||
@@ -104,10 +103,8 @@ export class HaStateLabelBadge extends LitElement {
|
||||
// 4. Icon determined via entity state
|
||||
// 5. Value string as fallback
|
||||
const domain = computeStateDomain(entityState);
|
||||
const entry = this.hass?.entities[entityState.entity_id];
|
||||
|
||||
const showIcon =
|
||||
this.icon || this._computeShowIcon(domain, entityState, entry);
|
||||
const showIcon = this.icon || this._computeShowIcon(domain, entityState);
|
||||
const image = this.icon
|
||||
? ""
|
||||
: this.image
|
||||
@@ -115,9 +112,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
: entityState.attributes.entity_picture_local ||
|
||||
entityState.attributes.entity_picture;
|
||||
const value =
|
||||
!image && !showIcon
|
||||
? this._computeValue(domain, entityState, entry)
|
||||
: undefined;
|
||||
!image && !showIcon ? this._computeValue(domain, entityState) : undefined;
|
||||
|
||||
return html`
|
||||
<ha-label-badge
|
||||
@@ -157,11 +152,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _computeValue(
|
||||
domain: string,
|
||||
entityState: HassEntity,
|
||||
entry?: EntityRegistryEntry
|
||||
) {
|
||||
private _computeValue(domain: string, entityState: HassEntity) {
|
||||
switch (domain) {
|
||||
case "alarm_control_panel":
|
||||
case "binary_sensor":
|
||||
@@ -174,7 +165,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
return null;
|
||||
// @ts-expect-error we don't break and go to default
|
||||
case "sensor":
|
||||
if (entry?.platform === "moon") {
|
||||
if (entityState.attributes.device_class === "moon__phase") {
|
||||
return null;
|
||||
}
|
||||
// eslint-disable-next-line: disable=no-fallthrough
|
||||
@@ -197,11 +188,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _computeShowIcon(
|
||||
domain: string,
|
||||
entityState: HassEntity,
|
||||
entry?: EntityRegistryEntry
|
||||
): boolean {
|
||||
private _computeShowIcon(domain: string, entityState: HassEntity): boolean {
|
||||
if (entityState.state === UNAVAILABLE) {
|
||||
return false;
|
||||
}
|
||||
@@ -217,7 +204,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
case "timer":
|
||||
return true;
|
||||
case "sensor":
|
||||
return entry?.platform === "moon";
|
||||
return entityState.attributes.device_class === "moon__phase";
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -236,10 +223,6 @@ export class HaStateLabelBadge extends LitElement {
|
||||
if (domainStateKey) {
|
||||
return this.hass!.localize(`state_badge.${domainStateKey}`);
|
||||
}
|
||||
// Person and device tracker state can be zone name
|
||||
if (domain === "person" || domain === "device_tracker") {
|
||||
return entityState.state;
|
||||
}
|
||||
if (domain === "timer") {
|
||||
return secondsToDuration(_timerTimeRemaining);
|
||||
}
|
||||
|
@@ -28,7 +28,7 @@ class StateInfo extends LitElement {
|
||||
|
||||
const name = computeStateName(this.stateObj);
|
||||
|
||||
return html`<state-badge
|
||||
return html` <state-badge
|
||||
.stateObj=${this.stateObj}
|
||||
.stateColor=${true}
|
||||
.color=${this.color}
|
||||
|
@@ -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*$/,
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -3,12 +3,10 @@ import { styles } from "@material/mwc-dialog/mwc-dialog.css";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
|
||||
import "./ha-icon-button";
|
||||
|
||||
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button"];
|
||||
|
||||
export const createCloseHeading = (
|
||||
hass: HomeAssistant,
|
||||
title: string | TemplateResult
|
||||
@@ -34,14 +32,6 @@ export class HaDialog extends DialogBase {
|
||||
return html`<slot name="heading"> ${super.renderHeading()} </slot>`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
super.firstUpdated();
|
||||
this.suppressDefaultPressSelector = [
|
||||
this.suppressDefaultPressSelector,
|
||||
SUPPRESS_DEFAULT_PRESS_SELECTOR,
|
||||
].join(", ");
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
|
@@ -67,9 +67,6 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
@change=${this._valueChanged}
|
||||
></ha-slider>
|
||||
</div>
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
: ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
@@ -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}
|
||||
>
|
||||
|
@@ -9,7 +9,6 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
getDeviceIntegrationLookup,
|
||||
@@ -79,7 +78,7 @@ export class HaTargetSelector extends LitElement {
|
||||
? [this.selector.target?.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.target?.entity?.domain
|
||||
? ensureArray(this.selector.target.entity.domain as string | string[])
|
||||
? [this.selector.target?.entity.domain]
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
></ha-target-picker>`;
|
||||
|
@@ -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,
|
||||
|
@@ -22,6 +22,7 @@ export interface EntityRegistryEntry {
|
||||
original_name?: string;
|
||||
unique_id: string;
|
||||
translation_key?: string;
|
||||
aliases: string[];
|
||||
}
|
||||
|
||||
export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
|
||||
@@ -29,7 +30,6 @@ export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
|
||||
original_icon?: string;
|
||||
device_class?: string;
|
||||
original_device_class?: string;
|
||||
aliases: string[];
|
||||
}
|
||||
|
||||
export interface UpdateEntityRegistryEntryResult {
|
||||
@@ -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;
|
||||
|
@@ -51,7 +51,7 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
|
||||
@state() private _summary = "";
|
||||
|
||||
@state() private _description? = "";
|
||||
@state() private _description = "";
|
||||
|
||||
@state() private _rrule?: string;
|
||||
|
||||
@@ -87,7 +87,6 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
const entry = params.entry!;
|
||||
this._allDay = isDate(entry.dtstart);
|
||||
this._summary = entry.summary;
|
||||
this._description = entry.description;
|
||||
this._rrule = entry.rrule;
|
||||
if (this._allDay) {
|
||||
this._dtstart = new Date(entry.dtstart + "T00:00:00");
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { formatInTimeZone, toDate } from "date-fns-tz";
|
||||
import { css, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
@@ -7,7 +6,6 @@ import type { Options, WeekdayStr } from "rrule";
|
||||
import { ByWeekday, RRule, Weekday } from "rrule";
|
||||
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { LocalizeKeys } from "../../common/translations/localize";
|
||||
import "../../components/ha-chip";
|
||||
import "../../components/ha-list-item";
|
||||
import "../../components/ha-select";
|
||||
@@ -21,10 +19,12 @@ import {
|
||||
getWeekday,
|
||||
getWeekdays,
|
||||
getMonthlyRepeatItems,
|
||||
intervalSuffix,
|
||||
RepeatEnd,
|
||||
RepeatFrequency,
|
||||
ruleByWeekDay,
|
||||
untilValue,
|
||||
WEEKDAY_NAME,
|
||||
MonthlyRepeatItem,
|
||||
getMonthlyRepeatWeekdayFromRule,
|
||||
getMonthdayRepeatFromRule,
|
||||
@@ -65,7 +65,7 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
|
||||
@state() private _count?: number;
|
||||
|
||||
@state() private _untilDay?: Date;
|
||||
@state() private _until?: Date;
|
||||
|
||||
@query("#monthly") private _monthlyRepeatSelect!: HaSelect;
|
||||
|
||||
@@ -98,17 +98,15 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
}
|
||||
|
||||
if (
|
||||
!changedProps.has("value") &&
|
||||
(changedProps.has("dtstart") ||
|
||||
changedProps.has("timezone") ||
|
||||
changedProps.has("_freq") ||
|
||||
changedProps.has("_interval") ||
|
||||
changedProps.has("_weekday") ||
|
||||
changedProps.has("_monthlyRepeatWeekday") ||
|
||||
changedProps.has("_monthday") ||
|
||||
changedProps.has("_end") ||
|
||||
changedProps.has("_count") ||
|
||||
changedProps.has("_untilDay"))
|
||||
changedProps.has("timezone") ||
|
||||
changedProps.has("_freq") ||
|
||||
changedProps.has("_interval") ||
|
||||
changedProps.has("_weekday") ||
|
||||
changedProps.has("_monthlyRepeatWeekday") ||
|
||||
changedProps.has("_monthday") ||
|
||||
changedProps.has("_end") ||
|
||||
changedProps.has("_count") ||
|
||||
changedProps.has("_until")
|
||||
) {
|
||||
this._updateRule();
|
||||
return;
|
||||
@@ -125,7 +123,7 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
this._monthlyRepeatWeekday = undefined;
|
||||
this._end = "never";
|
||||
this._count = undefined;
|
||||
this._untilDay = undefined;
|
||||
this._until = undefined;
|
||||
|
||||
this._computedRRule = this.value;
|
||||
if (this.value === "") {
|
||||
@@ -165,7 +163,7 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
}
|
||||
if (rrule.until) {
|
||||
this._end = "on";
|
||||
this._untilDay = toDate(rrule.until, { timeZone: this.timezone });
|
||||
this._until = rrule.until;
|
||||
} else if (rrule.count) {
|
||||
this._end = "after";
|
||||
this._count = rrule.count;
|
||||
@@ -176,36 +174,18 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
return html`
|
||||
<ha-select
|
||||
id="freq"
|
||||
label=${this.hass.localize("ui.components.calendar.event.repeat.label")}
|
||||
label="Repeat"
|
||||
@selected=${this._onRepeatSelected}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.value=${this._freq}
|
||||
>
|
||||
<ha-list-item value="none">
|
||||
${this.hass.localize("ui.components.calendar.event.repeat.freq.none")}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="yearly">
|
||||
${this.hass.localize(
|
||||
"ui.components.calendar.event.repeat.freq.yearly"
|
||||
)}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="monthly">
|
||||
${this.hass.localize(
|
||||
"ui.components.calendar.event.repeat.freq.monthly"
|
||||
)}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="weekly">
|
||||
${this.hass.localize(
|
||||
"ui.components.calendar.event.repeat.freq.weekly"
|
||||
)}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="daily">
|
||||
${this.hass.localize(
|
||||
"ui.components.calendar.event.repeat.freq.daily"
|
||||
)}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="none">None</ha-list-item>
|
||||
<ha-list-item value="yearly">Yearly</ha-list-item>
|
||||
<ha-list-item value="monthly">Monthly</ha-list-item>
|
||||
<ha-list-item value="weekly">Weekly</ha-list-item>
|
||||
<ha-list-item value="daily">Daily</ha-list-item>
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
@@ -216,9 +196,7 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
${this._monthlyRepeatItems.length > 0
|
||||
? html`<ha-select
|
||||
id="monthly"
|
||||
label=${this.hass.localize(
|
||||
"ui.components.calendar.event.repeat.monthly.label"
|
||||
)}
|
||||
label="Repeat Monthly"
|
||||
@selected=${this._onMonthlyDetailSelected}
|
||||
.value=${this._monthlyRepeat || this._monthlyRepeatItems[0]?.value}
|
||||
@closed=${stopPropagation}
|
||||
@@ -247,11 +225,7 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
.value=${item}
|
||||
class=${classMap({ active: this._weekday.has(item) })}
|
||||
@click=${this._onWeekdayToggle}
|
||||
>${this.hass.localize(
|
||||
`ui.components.calendar.event.repeat.weekly.weekday.${
|
||||
item.toLowerCase() as Lowercase<WeekdayStr>
|
||||
}`
|
||||
)}</ha-chip
|
||||
>${WEEKDAY_NAME[item]}</ha-chip
|
||||
>
|
||||
`
|
||||
)}
|
||||
@@ -267,16 +241,11 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
return html`
|
||||
<ha-textfield
|
||||
id="interval"
|
||||
label=${this.hass.localize(
|
||||
"ui.components.calendar.event.repeat.interval.label"
|
||||
)}
|
||||
label="Repeat interval"
|
||||
type="number"
|
||||
min="1"
|
||||
.value=${this._interval}
|
||||
.suffix=${this.hass.localize(
|
||||
`ui.components.calendar.event.repeat.interval.${this
|
||||
._freq!}` as LocalizeKeys
|
||||
)}
|
||||
.suffix=${intervalSuffix(this._freq!)}
|
||||
@change=${this._onIntervalChange}
|
||||
></ha-textfield>
|
||||
`;
|
||||
@@ -286,38 +255,26 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
return html`
|
||||
<ha-select
|
||||
id="end"
|
||||
label=${this.hass.localize(
|
||||
"ui.components.calendar.event.repeat.end.label"
|
||||
)}
|
||||
label="Ends"
|
||||
.value=${this._end}
|
||||
@selected=${this._onEndSelected}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
>
|
||||
<ha-list-item value="never">
|
||||
${this.hass.localize("ui.components.calendar.event.repeat.end.never")}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="after">
|
||||
${this.hass.localize("ui.components.calendar.event.repeat.end.after")}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="on">
|
||||
${this.hass.localize("ui.components.calendar.event.repeat.end.on")}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="never">Never</ha-list-item>
|
||||
<ha-list-item value="after">After</ha-list-item>
|
||||
<ha-list-item value="on">On</ha-list-item>
|
||||
</ha-select>
|
||||
${this._end === "after"
|
||||
? html`
|
||||
<ha-textfield
|
||||
id="after"
|
||||
label=${this.hass.localize(
|
||||
"ui.components.calendar.event.repeat.end_after.label"
|
||||
)}
|
||||
label="End after"
|
||||
type="number"
|
||||
min="1"
|
||||
.value=${this._count!}
|
||||
suffix=${this.hass.localize(
|
||||
"ui.components.calendar.event.repeat.end_after.ocurrences"
|
||||
)}
|
||||
suffix="ocurrences"
|
||||
@change=${this._onCountChange}
|
||||
></ha-textfield>
|
||||
`
|
||||
@@ -326,11 +283,9 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
? html`
|
||||
<ha-date-input
|
||||
id="on"
|
||||
label=${this.hass.localize(
|
||||
"ui.components.calendar.event.repeat.end_on.label"
|
||||
)}
|
||||
label="End on"
|
||||
.locale=${this.locale}
|
||||
.value=${this._formatDate(this._untilDay!)}
|
||||
.value=${this._until!.toISOString()}
|
||||
@value-changed=${this._onUntilChange}
|
||||
></ha-date-input>
|
||||
`
|
||||
@@ -384,7 +339,6 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
} else {
|
||||
this._weekday.delete(value);
|
||||
}
|
||||
this.requestUpdate("_weekday");
|
||||
}
|
||||
|
||||
private _onEndSelected(e: CustomEvent<SelectedDetail<number>>) {
|
||||
@@ -397,15 +351,15 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
switch (this._end) {
|
||||
case "after":
|
||||
this._count = DEFAULT_COUNT[this._freq!];
|
||||
this._untilDay = undefined;
|
||||
this._until = undefined;
|
||||
break;
|
||||
case "on":
|
||||
this._count = undefined;
|
||||
this._untilDay = untilValue(this._freq!);
|
||||
this._until = untilValue(this._freq!);
|
||||
break;
|
||||
default:
|
||||
this._count = undefined;
|
||||
this._untilDay = undefined;
|
||||
this._until = undefined;
|
||||
}
|
||||
e.stopPropagation();
|
||||
}
|
||||
@@ -416,9 +370,7 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
|
||||
private _onUntilChange(e: CustomEvent) {
|
||||
e.stopPropagation();
|
||||
this._untilDay = toDate(e.detail.value + "T00:00:00", {
|
||||
timeZone: this.timezone,
|
||||
});
|
||||
this._until = new Date(e.detail.value);
|
||||
}
|
||||
|
||||
// Reset the weekday selected when there is only a single value
|
||||
@@ -447,27 +399,18 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
freq: convertRepeatFrequency(this._freq!)!,
|
||||
interval: this._interval > 1 ? this._interval : undefined,
|
||||
count: this._count,
|
||||
until: this._until,
|
||||
tzid: this.timezone,
|
||||
byweekday: byweekday,
|
||||
bymonthday: bymonthday,
|
||||
};
|
||||
let contentline = RRule.optionsToString(options);
|
||||
if (this._untilDay) {
|
||||
// The UNTIL value should be inclusive of the last event instance
|
||||
const until = toDate(
|
||||
this._formatDate(this._untilDay!) +
|
||||
"T" +
|
||||
this._formatTime(this.dtstart!),
|
||||
{ timeZone: this.timezone }
|
||||
);
|
||||
// rrule.js can't compute some UNTIL variations so we compute that ourself. Must be
|
||||
// in the same format as dtstart.
|
||||
const format = this.allDay ? "yyyyMMdd" : "yyyyMMdd'T'HHmmss";
|
||||
const newUntilValue = formatInTimeZone(
|
||||
until,
|
||||
this.hass.config.time_zone,
|
||||
format
|
||||
);
|
||||
contentline += `;UNTIL=${newUntilValue}`;
|
||||
if (this._until && this.allDay) {
|
||||
// rrule.js only computes UNTIL values as DATE-TIME however rfc5545 says
|
||||
// The value of the UNTIL rule part MUST have the same value type as the
|
||||
// "DTSTART" property. If needed, strip off any time values as a workaround
|
||||
// This converts "UNTIL=20220512T060000" to "UNTIL=20220512"
|
||||
contentline = contentline.replace(/(UNTIL=\d{8})T\d{6}Z?/, "$1");
|
||||
}
|
||||
return contentline.slice(6); // Strip "RRULE:" prefix
|
||||
}
|
||||
@@ -487,16 +430,6 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
// Formats a date in browser display timezone
|
||||
private _formatDate(date: Date): string {
|
||||
return formatInTimeZone(date, this.timezone!, "yyyy-MM-dd");
|
||||
}
|
||||
|
||||
// Formats a time in browser display timezone
|
||||
private _formatTime(date: Date): string {
|
||||
return formatInTimeZone(date, this.timezone!, "HH:mm:ss");
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-textfield,
|
||||
ha-select {
|
||||
|
@@ -42,6 +42,16 @@ export interface MonthlyRepeatItem {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export function intervalSuffix(freq: RepeatFrequency) {
|
||||
if (freq === "monthly") {
|
||||
return "months";
|
||||
}
|
||||
if (freq === "weekly") {
|
||||
return "weeks";
|
||||
}
|
||||
return "days";
|
||||
}
|
||||
|
||||
export function untilValue(freq: RepeatFrequency): Date {
|
||||
const today = new Date();
|
||||
const increment = DEFAULT_COUNT[freq];
|
||||
@@ -92,6 +102,16 @@ export const convertRepeatFrequency = (
|
||||
}
|
||||
};
|
||||
|
||||
export const WEEKDAY_NAME = {
|
||||
SU: "Sun",
|
||||
MO: "Mon",
|
||||
TU: "Tue",
|
||||
WE: "Wed",
|
||||
TH: "Thu",
|
||||
FR: "Fri",
|
||||
SA: "Sat",
|
||||
};
|
||||
|
||||
export const WEEKDAYS = [
|
||||
RRule.SU,
|
||||
RRule.MO,
|
||||
|
@@ -158,7 +158,6 @@ export class HaDeviceAction extends LitElement {
|
||||
}
|
||||
|
||||
ha-form {
|
||||
display: block;
|
||||
margin-top: 24px;
|
||||
}
|
||||
`;
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -184,7 +184,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as any;
|
||||
const key = target.key;
|
||||
const value = ev.detail ? ev.detail.value : target.value;
|
||||
const value = ev.detail?.value ?? target.value;
|
||||
if (
|
||||
(this.config.use_blueprint.input &&
|
||||
this.config.use_blueprint.input[key] === value) ||
|
||||
|
@@ -541,10 +541,15 @@ export class HaAutomationTrace extends LitElement {
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
background-color: var(--card-background-color);
|
||||
}
|
||||
|
||||
.linkButton {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.trace-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
@@ -46,7 +46,7 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
|
||||
],
|
||||
],
|
||||
},
|
||||
{ name: "offset", selector: { duration: {} } },
|
||||
{ name: "offset", selector: { duration: { enable_day: true } } },
|
||||
{
|
||||
name: "offset_type",
|
||||
type: "select",
|
||||
|
@@ -9,6 +9,7 @@ import {
|
||||
mdiFormatListChecks,
|
||||
mdiSync,
|
||||
} from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
@@ -40,17 +41,22 @@ import {
|
||||
updateCloudAlexaEntityConfig,
|
||||
updateCloudPref,
|
||||
} from "../../../../data/cloud";
|
||||
import { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../../data/entity_registry";
|
||||
import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler";
|
||||
import "../../../../layouts/hass-loading-screen";
|
||||
import "../../../../layouts/hass-subpage";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
const DEFAULT_CONFIG_EXPOSE = true;
|
||||
const IGNORE_INTERFACES = ["Alexa.EndpointHealth"];
|
||||
|
||||
@customElement("cloud-alexa")
|
||||
class CloudAlexa extends LitElement {
|
||||
class CloudAlexa extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property()
|
||||
@@ -162,8 +168,13 @@ class CloudAlexa extends LitElement {
|
||||
<state-info
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
secondary-line
|
||||
@click=${this._showMoreInfo}
|
||||
>
|
||||
${entity.interfaces
|
||||
.filter((ifc) => !IGNORE_INTERFACES.includes(ifc))
|
||||
.map((ifc) => ifc.replace(/(Alexa.|Controller)/g, ""))
|
||||
.join(", ")}
|
||||
</state-info>
|
||||
${!emptyFilter
|
||||
? html`${iconButton}`
|
||||
@@ -312,18 +323,23 @@ class CloudAlexa extends LitElement {
|
||||
if (changedProps.has("cloudStatus")) {
|
||||
this._entityConfigs = this.cloudStatus.prefs.alexa_entity_configs;
|
||||
}
|
||||
if (
|
||||
changedProps.has("hass") &&
|
||||
changedProps.get("hass")?.entities !== this.hass.entities
|
||||
) {
|
||||
const categories = {};
|
||||
}
|
||||
|
||||
for (const entry of Object.values(this.hass.entities)) {
|
||||
categories[entry.entity_id] = entry.entity_category;
|
||||
}
|
||||
protected override hassSubscribe(): (
|
||||
| UnsubscribeFunc
|
||||
| Promise<UnsubscribeFunc>
|
||||
)[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection, (entries) => {
|
||||
const categories = {};
|
||||
|
||||
this._entityCategories = categories;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
categories[entry.entity_id] = entry.entity_category;
|
||||
}
|
||||
|
||||
this._entityCategories = categories;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
@@ -526,7 +542,6 @@ class CloudAlexa extends LitElement {
|
||||
}
|
||||
state-info {
|
||||
cursor: pointer;
|
||||
height: 40px;
|
||||
}
|
||||
ha-switch {
|
||||
padding: 8px 0;
|
||||
|
@@ -9,6 +9,7 @@ import {
|
||||
mdiFormatListChecks,
|
||||
mdiSync,
|
||||
} from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
@@ -40,9 +41,7 @@ import {
|
||||
} from "../../../../data/cloud";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
ExtEntityRegistryEntry,
|
||||
getExtendedEntityRegistryEntries,
|
||||
updateEntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../../data/entity_registry";
|
||||
import {
|
||||
fetchCloudGoogleEntities,
|
||||
@@ -52,15 +51,15 @@ import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show
|
||||
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../../layouts/hass-loading-screen";
|
||||
import "../../../../layouts/hass-subpage";
|
||||
import { buttonLinkStyle, haStyle } from "../../../../resources/styles";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import { showEntityAliasesDialog } from "../../entities/entity-aliases/show-dialog-entity-aliases";
|
||||
|
||||
const DEFAULT_CONFIG_EXPOSE = true;
|
||||
|
||||
@customElement("cloud-google-assistant")
|
||||
class CloudGoogleAssistant extends LitElement {
|
||||
class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public cloudStatus!: CloudStatusLoggedIn;
|
||||
@@ -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,57 +174,15 @@ 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.traits
|
||||
.map((trait) => trait.substr(trait.lastIndexOf(".") + 1))
|
||||
.join(", ")}
|
||||
</state-info>
|
||||
${!emptyFilter
|
||||
? html`${iconButton}`
|
||||
: html`<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
.entityId=${entity.entity_id}
|
||||
.entityId=${stateObj.entity_id}
|
||||
@action=${this._exposeChanged}
|
||||
>
|
||||
${iconButton}
|
||||
@@ -355,7 +308,7 @@ class CloudGoogleAssistant extends LitElement {
|
||||
</h3>
|
||||
${!this.narrow
|
||||
? this.hass!.localize(
|
||||
"ui.panel.config.cloud.google.exposed",
|
||||
"ui.panel.config.cloud.alexa.exposed",
|
||||
"selected",
|
||||
selected
|
||||
)
|
||||
@@ -376,7 +329,7 @@ class CloudGoogleAssistant extends LitElement {
|
||||
</h3>
|
||||
${!this.narrow
|
||||
? this.hass!.localize(
|
||||
"ui.panel.config.cloud.google.not_exposed",
|
||||
"ui.panel.config.cloud.alexa.not_exposed",
|
||||
"selected",
|
||||
this._entities.length - selected
|
||||
)
|
||||
@@ -401,38 +354,23 @@ class CloudGoogleAssistant extends LitElement {
|
||||
if (changedProps.has("cloudStatus")) {
|
||||
this._entityConfigs = this.cloudStatus.prefs.google_entity_configs;
|
||||
}
|
||||
if (
|
||||
changedProps.has("hass") &&
|
||||
changedProps.get("hass")?.entities !== this.hass.entities
|
||||
) {
|
||||
const categories = {};
|
||||
|
||||
for (const entry of Object.values(this.hass.entities)) {
|
||||
categories[entry.entity_id] = entry.entity_category;
|
||||
}
|
||||
|
||||
this._entityCategories = categories;
|
||||
}
|
||||
}
|
||||
|
||||
private async _openAliasesSettings(ev) {
|
||||
ev.stopPropagation();
|
||||
const entityId = ev.target.entityId;
|
||||
const entry = this._entries![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;
|
||||
},
|
||||
});
|
||||
protected override hassSubscribe(): (
|
||||
| UnsubscribeFunc
|
||||
| Promise<UnsubscribeFunc>
|
||||
)[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection, (entries) => {
|
||||
const categories = {};
|
||||
|
||||
for (const entry of entries) {
|
||||
categories[entry.entity_id] = entry.entity_category;
|
||||
}
|
||||
|
||||
this._entityCategories = categories;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
private _configIsDomainExposed(
|
||||
@@ -459,13 +397,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 +411,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>) {
|
||||
@@ -659,7 +583,6 @@ class CloudGoogleAssistant extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
buttonLinkStyle,
|
||||
css`
|
||||
mwc-list-item > [slot="meta"] {
|
||||
margin-left: 4px;
|
||||
|
@@ -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>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
@@ -57,13 +57,7 @@ export class EntityRegistrySettingsHelper extends LitElement {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("entry")) {
|
||||
this._error = undefined;
|
||||
if (
|
||||
this.entry.unique_id !==
|
||||
(changedProperties.get("entry") as ExtEntityRegistryEntry)?.unique_id
|
||||
) {
|
||||
this._item = undefined;
|
||||
}
|
||||
|
||||
this._item = undefined;
|
||||
this._getItem();
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
@@ -72,21 +72,16 @@ class DialogEntityAliases extends LitElement {
|
||||
dialogInitialFocus=${index}
|
||||
.index=${index}
|
||||
class="flex-auto"
|
||||
.label=${this.hass!.localize(
|
||||
"ui.dialogs.entity_registry.editor.aliases.input_label",
|
||||
{ number: index + 1 }
|
||||
)}
|
||||
label="Alias"
|
||||
.value=${alias}
|
||||
?data-last=${index === this._aliases.length - 1}
|
||||
@input=${this._editAlias}
|
||||
@keydown=${this._keyDownAlias}
|
||||
@change=${this._editAlias}
|
||||
></ha-textfield>
|
||||
<ha-icon-button
|
||||
.index=${index}
|
||||
slot="navigationIcon"
|
||||
label=${this.hass!.localize(
|
||||
"ui.dialogs.entity_registry.editor.aliases.remove_alias",
|
||||
{ number: index + 1 }
|
||||
"ui.dialogs.entity_registry.editor.aliases.remove_alias"
|
||||
)}
|
||||
@click=${this._removeAlias}
|
||||
.path=${mdiDeleteOutline}
|
||||
@@ -138,13 +133,6 @@ class DialogEntityAliases extends LitElement {
|
||||
this._aliases[index] = (ev.target as any).value;
|
||||
}
|
||||
|
||||
private async _keyDownAlias(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter") {
|
||||
ev.stopPropagation();
|
||||
this._addAlias();
|
||||
}
|
||||
}
|
||||
|
||||
private async _removeAlias(ev: Event) {
|
||||
const index = (ev.target as any).index;
|
||||
const aliases = [...this._aliases];
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
EntityRegistryEntryUpdateParams,
|
||||
ExtEntityRegistryEntry,
|
||||
} from "../../../../data/entity_registry";
|
||||
|
||||
export interface EntityAliasesDialogParams {
|
||||
entity: ExtEntityRegistryEntry;
|
||||
entity: EntityRegistryEntry;
|
||||
updateEntry: (
|
||||
updates: Partial<EntityRegistryEntryUpdateParams>
|
||||
) => Promise<unknown>;
|
||||
|
@@ -1,13 +1,8 @@
|
||||
import "@material/mwc-formfield/mwc-formfield";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiPencil } from "@mdi/js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
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";
|
||||
@@ -26,7 +21,6 @@ import {
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showEntityAliasesDialog } from "./entity-aliases/show-dialog-entity-aliases";
|
||||
|
||||
@customElement("ha-registry-basic-editor")
|
||||
export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
@@ -50,21 +44,6 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
private _handleAliasesClicked(ev: CustomEvent) {
|
||||
if (ev.detail.index !== 0) return;
|
||||
showEntityAliasesDialog(this, {
|
||||
entity: this.entry!,
|
||||
updateEntry: async (updates) => {
|
||||
const result = await updateEntityRegistryEntry(
|
||||
this.hass,
|
||||
this.entry.entity_id,
|
||||
updates
|
||||
);
|
||||
fireEvent(this, "entity-entry-updated", result.entity_entry);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async updateEntry(): Promise<void> {
|
||||
this._submitting = true;
|
||||
const params: Partial<EntityRegistryEntryUpdateParams> = {
|
||||
@@ -268,37 +247,6 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<div class="label">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.aliases_section"
|
||||
)}
|
||||
</div>
|
||||
<mwc-list class="aliases" @action=${this._handleAliasesClicked}>
|
||||
<mwc-list-item .twoline=${this.entry.aliases.length > 0} hasMeta>
|
||||
<span>
|
||||
${this.entry.aliases.length > 0
|
||||
? this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.configured_aliases",
|
||||
{ count: this.entry.aliases.length }
|
||||
)
|
||||
: this.hass.localize(
|
||||
"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>
|
||||
<ha-svg-icon slot="meta" .path=${mdiPencil}></ha-svg-icon>
|
||||
</mwc-list-item>
|
||||
</mwc-list>
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.aliases.description"
|
||||
)}
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
@@ -352,13 +300,6 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
.label {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.aliases {
|
||||
border-radius: 4px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
--mdc-icon-button-size: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -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,22 @@ const OVERRIDE_NUMBER_UNITS = {
|
||||
temperature: ["°C", "°F", "K"],
|
||||
};
|
||||
|
||||
const OVERRIDE_SENSOR_UNITS = {
|
||||
current: ["A", "mA"],
|
||||
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 +187,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 +290,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 +434,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 +448,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>
|
||||
`
|
||||
@@ -774,8 +771,12 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
"ui.dialogs.entity_registry.editor.aliases_section"
|
||||
)}
|
||||
</div>
|
||||
<mwc-list class="aliases" @action=${this._handleAliasesClicked}>
|
||||
<mwc-list-item .twoline=${this.entry.aliases.length > 0} hasMeta>
|
||||
<mwc-list class="aliases">
|
||||
<mwc-list-item
|
||||
.twoline=${this.entry.aliases.length > 0}
|
||||
hasMeta
|
||||
@click=${this._openAliasesSettings}
|
||||
>
|
||||
<span>
|
||||
${this.entry.aliases.length > 0
|
||||
? this.hass.localize(
|
||||
@@ -786,13 +787,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>
|
||||
@@ -984,8 +979,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleAliasesClicked(ev: CustomEvent) {
|
||||
if (ev.detail.index !== 0) return;
|
||||
private _openAliasesSettings() {
|
||||
showEntityAliasesDialog(this, {
|
||||
entity: this.entry!,
|
||||
updateEntry: async (updates) => {
|
||||
|
@@ -728,6 +728,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
selectable: false,
|
||||
entity_category: null,
|
||||
has_entity_name: false,
|
||||
aliases: [],
|
||||
});
|
||||
}
|
||||
if (changed) {
|
||||
|
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -40,14 +40,10 @@ class HaPanelDevMqtt extends LitElement {
|
||||
return html`
|
||||
<hass-subpage .narrow=${this.narrow} .hass=${this.hass}>
|
||||
<div class="content">
|
||||
<ha-card
|
||||
.header=${this.hass.localize("ui.panel.config.mqtt.settings_title")}
|
||||
>
|
||||
<ha-card header="MQTT settings">
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._openOptionFlow}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.mqtt.reconfigure"
|
||||
)}</mwc-button
|
||||
>Re-configure MQTT</mwc-button
|
||||
>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -158,7 +158,7 @@ export class HaBlueprintScriptEditor extends LitElement {
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as any;
|
||||
const key = target.key;
|
||||
const value = ev.detail ? ev.detail.value : target.value;
|
||||
const value = ev.detail?.value ?? target.value;
|
||||
if (
|
||||
(this.config.use_blueprint.input &&
|
||||
this.config.use_blueprint.input[key] === value) ||
|
||||
|
@@ -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:
|
||||
|
@@ -527,10 +527,15 @@ export class HaScriptTrace extends LitElement {
|
||||
:host([narrow]) .graph {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
background-color: var(--card-background-color);
|
||||
}
|
||||
|
||||
.linkButton {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.trace-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -59,7 +59,7 @@ const splitByAreaDevice = (
|
||||
for (const entity of Object.values(entityEntries)) {
|
||||
const areaId =
|
||||
entity.area_id ||
|
||||
(entity.device_id && deviceEntries[entity.device_id]?.area_id);
|
||||
(entity.device_id && deviceEntries[entity.device_id].area_id);
|
||||
if (areaId && areaId in areaEntries && entity.entity_id in allEntities) {
|
||||
if (!(areaId in areasWithEntities)) {
|
||||
areasWithEntities[areaId] = [];
|
||||
@@ -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);
|
||||
};
|
||||
|
@@ -46,7 +46,7 @@ export const handleAction = async (
|
||||
actionConfig.confirmation &&
|
||||
(!actionConfig.confirmation.exemptions ||
|
||||
!actionConfig.confirmation.exemptions.some(
|
||||
(e) => e.user === hass!.user?.id
|
||||
(e) => e.user === hass!.user!.id
|
||||
))
|
||||
) {
|
||||
forwardHaptic("warning");
|
||||
|
@@ -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,
|
||||
@@ -266,7 +266,7 @@ class HUIRoot extends LitElement {
|
||||
((Array.isArray(view.visible) &&
|
||||
!view.visible.some(
|
||||
(e) =>
|
||||
e.user === this.hass!.user?.id
|
||||
e.user === this.hass!.user!.id
|
||||
)) ||
|
||||
view.visible === false))
|
||||
),
|
||||
@@ -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>
|
||||
`
|
||||
@@ -470,7 +470,7 @@ class HUIRoot extends LitElement {
|
||||
view.visible !== undefined &&
|
||||
((Array.isArray(view.visible) &&
|
||||
!view.visible.some(
|
||||
(e) => e.user === this.hass!.user?.id
|
||||
(e) => e.user === this.hass!.user!.id
|
||||
)) ||
|
||||
view.visible === false)
|
||||
),
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -644,33 +644,6 @@
|
||||
"monthly": "months",
|
||||
"weekly": "weeks",
|
||||
"daily": "days"
|
||||
},
|
||||
"monthly": {
|
||||
"label": "Repeat Monthly"
|
||||
},
|
||||
"weekly": {
|
||||
"weekday": {
|
||||
"su": "Sun",
|
||||
"mo": "Mon",
|
||||
"tu": "Tue",
|
||||
"we": "Wed",
|
||||
"th": "Thu",
|
||||
"fr": "Fri",
|
||||
"sa": "Sat"
|
||||
}
|
||||
},
|
||||
"end": {
|
||||
"label": "End",
|
||||
"never": "Never",
|
||||
"after": "After",
|
||||
"on": "On"
|
||||
},
|
||||
"end_on": {
|
||||
"label": "End On"
|
||||
},
|
||||
"end_after": {
|
||||
"label": "End After",
|
||||
"ocurrences": "ocurrences"
|
||||
}
|
||||
},
|
||||
"rrule": {
|
||||
@@ -803,15 +776,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",
|
||||
@@ -1005,8 +976,7 @@
|
||||
"aliases": {
|
||||
"heading": "{name} aliases",
|
||||
"description": "Aliases are alternative names used in voice assistants to refer to this entity.",
|
||||
"remove_alias": "Remove alias {number}",
|
||||
"input_label": "Alias {number}",
|
||||
"remove_alias": "Remove alias",
|
||||
"save": "Save",
|
||||
"add_alias": "Add alias",
|
||||
"no_aliases": "No aliases have been added yet",
|
||||
@@ -1035,8 +1005,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 +1605,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": {
|
||||
@@ -2605,7 +2634,7 @@
|
||||
"enable_state_reporting": "Enable State Reporting",
|
||||
"info_state_reporting": "If you enable state reporting, Home Assistant will send all state changes of exposed entities to Amazon. This allows you to always see the latest states in the Alexa app and use the state changes to create routines.",
|
||||
"state_reporting_error": "Unable to {enable_disable} report state.",
|
||||
"manage_entities": "[%key:ui::panel::config::cloud::account::google::manage_entities%]",
|
||||
"manage_entities": "Manage Entities",
|
||||
"enable": "enable",
|
||||
"disable": "disable",
|
||||
"not_configured_title": "Alexa is not activated",
|
||||
@@ -2656,7 +2685,6 @@
|
||||
"follow_domain": "[%key:ui::panel::config::cloud::google::follow_domain%]",
|
||||
"exposed": "[%key:ui::panel::config::cloud::google::exposed%]",
|
||||
"not_exposed": "[%key:ui::panel::config::cloud::google::not_exposed%]",
|
||||
"manage_aliases": "[%key:ui::panel::config::cloud::google::manage_aliases%]",
|
||||
"expose": "Expose to Alexa",
|
||||
"sync_entities": "Synchronize entities",
|
||||
"sync_entities_error": "Failed to sync entities:"
|
||||
@@ -2682,11 +2710,6 @@
|
||||
"follow_domain": "Follow domain",
|
||||
"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:",
|
||||
@@ -3192,8 +3215,6 @@
|
||||
},
|
||||
"mqtt": {
|
||||
"title": "MQTT",
|
||||
"settings_title": "MQTT settings",
|
||||
"reconfigure": "Re-configure MQTT",
|
||||
"description_publish": "Publish a packet",
|
||||
"topic": "Topic",
|
||||
"payload": "Payload (template allowed)",
|
||||
@@ -3858,7 +3879,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