Compare commits

..

11 Commits

Author SHA1 Message Date
Paul Bottein
24e6b8483e 20230110.0 (#15070) 2023-01-10 19:24:17 +01:00
Paul Bottein
604c452ff4 Bumped version to 20230110.0 2023-01-10 19:04:27 +01:00
Paul Bottein
ba9551b61e Fixes weekday calendar chips toggle (#14990) 2023-01-10 18:33:29 +01:00
Paul Bottein
135af5bcaa Remove aliases configuration from alexa cloud page (#15003) 2023-01-10 18:33:16 +01:00
Allen Porter
747f47524e Fix UNTIL values to be inclusive of last event and bug in populating recurrence rules when editing calendar events (#15024)
* Fix bug in populating recurrence rules when editing calendar events

* Set UNTIL value to be inclusive of the last instance

* Fix lint errors
2023-01-10 18:32:18 +01:00
Paul Bottein
36b959dbc4 Fixes multiple domains target selector in blueprint (#15054)
* Fixes multiple domains target selector in blueprint

* Fixes lint
2023-01-10 18:31:22 +01:00
Paul Bottein
1d15f81b6c Fixes moon badge icon (#15015) 2023-01-10 18:30:21 +01:00
Bram Kragten
caa852559f 20230104.0 (#14985) 2023-01-04 11:43:34 +01:00
Bram Kragten
ebb19e4ed5 20230102.0 (#14963) 2023-01-02 21:42:03 +01:00
Bram Kragten
7cde3b66dd 20221230.0 (#14925) 2022-12-30 13:39:58 +01:00
Bram Kragten
2b8f7c46ff 20221228.0 (#14901) 2022-12-28 15:04:30 +01:00
75 changed files with 2531 additions and 2887 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -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

View 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>

View File

@@ -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])
)
);

View File

@@ -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)

View File

@@ -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)) {

View File

@@ -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)) {

View File

@@ -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": {

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20230104.0"
version = "20230110.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

View File

@@ -201,7 +201,6 @@ export const DOMAINS_WITH_CARD = [
export const SENSOR_ENTITIES = [
"sensor",
"binary_sensor",
"calendar",
"camera",
"device_tracker",
"weather",

View File

@@ -39,5 +39,5 @@ export default function scrollToTarget(element, target) {
);
requestAnimationFrame(updateFrame.bind(element));
}
}).call(element);
}.call(element));
}

View File

@@ -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"

View File

@@ -23,9 +23,7 @@ const STATIC_ACTIVE_COLORED_DOMAIN = new Set([
"input_boolean",
"light",
"media_player",
"plant",
"remote",
"schedule",
"script",
"siren",
"switch",

View File

@@ -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 {

View File

@@ -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 || [];

View File

@@ -35,9 +35,9 @@ const TRUNCATED_DOMAINS = [
"person",
] as const satisfies ReadonlyArray<keyof typeof FIXED_DOMAIN_STATES>;
type TruncatedDomain = (typeof TRUNCATED_DOMAINS)[number];
type TruncatedDomain = typeof TRUNCATED_DOMAINS[number];
type TruncatedKey = {
[T in TruncatedDomain]: `${T}.${(typeof FIXED_DOMAIN_STATES)[T][number]}`;
[T in TruncatedDomain]: `${T}.${typeof FIXED_DOMAIN_STATES[T][number]}`;
}[TruncatedDomain];
const getTruncatedKey = (domainKey: string, stateKey: string) => {

View File

@@ -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);

View File

@@ -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;

View File

@@ -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*$/,
};
}

View File

@@ -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}
>

View File

@@ -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
View 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);
});
};

View File

@@ -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,
});

View File

@@ -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,

View File

@@ -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,

View File

@@ -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.

View File

@@ -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"];

View File

@@ -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[];

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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,
});

View File

@@ -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",
});

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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;

View File

@@ -40,8 +40,7 @@ import {
} from "../../../../data/cloud";
import {
EntityRegistryEntry,
ExtEntityRegistryEntry,
getExtendedEntityRegistryEntries,
getExtendedEntityRegistryEntry,
updateEntityRegistryEntry,
} from "../../../../data/entity_registry";
import {
@@ -69,8 +68,6 @@ class CloudGoogleAssistant extends LitElement {
@state() private _entities?: GoogleEntity[];
@state() private _entries?: { [id: string]: ExtEntityRegistryEntry };
@state() private _syncing = false;
@state()
@@ -167,8 +164,6 @@ class CloudGoogleAssistant extends LitElement {
: mdiCloseBoxMultiple}
></ha-icon-button>`;
const aliases = this._entries?.[entity.entity_id]?.aliases;
target.push(html`
<ha-card outlined>
<div class="card-content">
@@ -179,51 +174,17 @@ class CloudGoogleAssistant extends LitElement {
secondary-line
@click=${this._showMoreInfo}
>
${aliases
? html`
<span>
${aliases.length > 0
? [...aliases]
.sort((a, b) =>
stringCompare(a, b, this.hass.locale.language)
)
.join(", ")
: this.hass.localize(
"ui.panel.config.cloud.google.no_aliases"
)}
</span>
<br />
<button
class="link"
.entityId=${entity.entity_id}
@click=${this._openAliasesSettings}
>
${this.hass.localize(
`ui.panel.config.cloud.google.${
aliases.length > 0
? "manage_aliases"
: "add_aliases"
}`
)}
</button>
`
: html`
<span>
${this.hass.localize(
"ui.panel.config.cloud.google.aliases_not_available"
)}
</span>
<br />
<button
class="link"
.stateObj=${stateObj}
@click=${this._showMoreInfoSettings}
>
${this.hass.localize(
"ui.panel.config.cloud.google.aliases_not_available_learn_more"
)}
</button>
`}
${entity.entity_id in this.hass.entities
? html`<button
class="link"
.entityId=${entity.entity_id}
@click=${this._openAliasesSettings}
>
${this.hass.localize(
"ui.panel.config.cloud.google.manage_aliases"
)}
</button>`
: ""}
</state-info>
${!emptyFilter
? html`${iconButton}`
@@ -418,19 +379,14 @@ class CloudGoogleAssistant extends LitElement {
private async _openAliasesSettings(ev) {
ev.stopPropagation();
const entityId = ev.target.entityId;
const entry = this._entries![entityId];
const entry = await getExtendedEntityRegistryEntry(this.hass, entityId);
if (!entry) {
return;
}
showEntityAliasesDialog(this, {
entity: entry,
updateEntry: async (updates) => {
const { entity_entry } = await updateEntityRegistryEntry(
this.hass,
entry.entity_id,
updates
);
this._entries![entity_entry.entity_id] = entity_entry;
await updateEntityRegistryEntry(this.hass, entry.entity_id, updates);
},
});
}
@@ -459,13 +415,6 @@ class CloudGoogleAssistant extends LitElement {
private async _fetchData() {
const entities = await fetchCloudGoogleEntities(this.hass);
this._entries = await getExtendedEntityRegistryEntries(
this.hass,
entities
.filter((ent) => this.hass.entities[ent.entity_id])
.map((e) => e.entity_id)
);
entities.sort((a, b) => {
const stateA = this.hass.states[a.entity_id];
const stateB = this.hass.states[b.entity_id];
@@ -480,14 +429,7 @@ class CloudGoogleAssistant extends LitElement {
private _showMoreInfo(ev) {
const entityId = ev.currentTarget.stateObj.entity_id;
const moreInfoTab = ev.currentTarget.moreInfoTab;
fireEvent(this, "hass-more-info", { entityId, tab: moreInfoTab });
}
private _showMoreInfoSettings(ev) {
ev.stopPropagation();
const entityId = ev.currentTarget.stateObj.entity_id;
fireEvent(this, "hass-more-info", { entityId, tab: "settings" });
fireEvent(this, "hass-more-info", { entityId });
}
private async _exposeChanged(ev: CustomEvent<ActionDetail>) {

View File

@@ -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>
`
);
}

View File

@@ -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;
}

View File

@@ -7,7 +7,6 @@ import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stringCompare } from "../../../common/string/compare";
import "../../../components/ha-area-picker";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-radio";
@@ -286,11 +285,7 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
"ui.dialogs.entity_registry.editor.no_aliases"
)}
</span>
<span slot="secondary">
${[...this.entry.aliases]
.sort((a, b) => stringCompare(a, b, this.hass.locale.language))
.join(", ")}
</span>
<span slot="secondary">${this.entry.aliases.join(", ")}</span>
<ha-svg-icon slot="meta" .path=${mdiPencil}></ha-svg-icon>
</mwc-list-item>
</mwc-list>

View File

@@ -67,7 +67,6 @@ import {
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import { domainToName } from "../../../data/integration";
import { getSensorDeviceClassConvertibleUnits } from "../../../data/sensor";
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
import {
showAlertDialog,
@@ -118,6 +117,58 @@ const OVERRIDE_NUMBER_UNITS = {
temperature: ["°C", "°F", "K"],
};
const OVERRIDE_SENSOR_UNITS = {
current: ["A", "mA"],
data_rate: [
"bit/s",
"kbit/s",
"Mbit/s",
"Gbit/s",
"B/s",
"kB/s",
"MB/s",
"GB/s",
"KiB/s",
"MiB/s",
"GiB/s",
],
data_size: [
"bit",
"kbit",
"Mbit",
"Gbit",
"B",
"kB",
"MB",
"GB",
"TB",
"PB",
"EB",
"ZB",
"YB",
"KiB",
"MiB",
"GiB",
"TiB",
"PiB",
"EiB",
"ZiB",
"YiB",
],
distance: ["cm", "ft", "in", "km", "m", "mi", "mm", "yd"],
gas: ["CCF", "ft³", "m³"],
precipitation: ["cm", "in", "mm"],
precipitation_intensity: ["in/d", "in/h", "mm/d", "mm/h"],
pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"],
speed: ["ft/s", "in/d", "in/h", "km/h", "kn", "m/s", "mm/d", "mm/h", "mph"],
temperature: ["°C", "°F", "K"],
voltage: ["V", "mV"],
volume: ["CCF", "fl. oz.", "ft³", "gal", "L", "mL", "m³"],
water: ["CCF", "ft³", "gal", "L", "m³"],
weight: ["g", "kg", "lb", "mg", "oz", "st", "µg"],
wind_speed: ["ft/s", "km/h", "kn", "mph", "m/s"],
};
const OVERRIDE_WEATHER_UNITS = {
precipitation: ["mm", "in"],
pressure: ["hPa", "mbar", "mmHg", "inHg"],
@@ -172,8 +223,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@state() private _cameraPrefs?: CameraPreferences;
@state() private _sensorDeviceClassConvertibleUnits?: string[];
private _origEntityId!: string;
private _deviceLookup?: Record<string, DeviceRegistryEntry>;
@@ -277,22 +326,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
}
}
protected async updated(changedProps: PropertyValues): Promise<void> {
if (changedProps.has("_deviceClass")) {
const domain = computeDomain(this.entry.entity_id);
if (domain === "sensor" && this._deviceClass) {
const { units } = await getSensorDeviceClassConvertibleUnits(
this.hass,
this._deviceClass
);
this._sensorDeviceClassConvertibleUnits = units;
} else {
this._sensorDeviceClassConvertibleUnits = [];
}
}
}
protected render(): TemplateResult {
if (this.entry.entity_id !== this._origEntityId) {
return html``;
@@ -437,7 +470,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
${domain === "sensor" &&
this._deviceClass &&
stateObj?.attributes.unit_of_measurement &&
this._sensorDeviceClassConvertibleUnits?.includes(
OVERRIDE_SENSOR_UNITS[this._deviceClass]?.includes(
stateObj?.attributes.unit_of_measurement
)
? html`
@@ -451,7 +484,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@selected=${this._unitChanged}
@closed=${stopPropagation}
>
${this._sensorDeviceClassConvertibleUnits.map(
${OVERRIDE_SENSOR_UNITS[this._deviceClass].map(
(unit: string) => html`
<mwc-list-item .value=${unit}>${unit}</mwc-list-item>
`
@@ -786,13 +819,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
"ui.dialogs.entity_registry.editor.no_aliases"
)}
</span>
<span slot="secondary">
${[...this.entry.aliases]
.sort((a, b) =>
stringCompare(a, b, this.hass.locale.language)
)
.join(", ")}
</span>
<span slot="secondary">${this.entry.aliases.join(", ")}</span>
<ha-svg-icon slot="meta" .path=${mdiPencil}></ha-svg-icon>
</mwc-list-item>
</mwc-list>

View File

@@ -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: () =>

View File

@@ -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")) {

View File

@@ -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 =

View File

@@ -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>
`

View File

@@ -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")

View File

@@ -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;
}
`,
];
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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:

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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);
}
`;
}
}

View File

@@ -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,

View File

@@ -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);
};

View File

@@ -16,4 +16,4 @@ export const TIMESTAMP_RENDERING_FORMATS = [
] as const;
export type TimestampRenderingFormat =
(typeof TIMESTAMP_RENDERING_FORMATS)[number];
typeof TIMESTAMP_RENDERING_FORMATS[number];

View File

@@ -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",

View File

@@ -102,7 +102,6 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow {
}
ha-select {
width: 100%;
--ha-select-min-width: 0;
}
`;
}

View File

@@ -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 {

View File

@@ -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";

View File

@@ -3,13 +3,13 @@ import "@material/mwc-list/mwc-list-item";
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import {
mdiCodeBraces,
mdiCommentProcessingOutline,
mdiDotsVertical,
mdiFileMultiple,
mdiFormatListBulletedTriangle,
mdiHelp,
mdiHelpCircle,
mdiMagnify,
mdiMicrophone,
mdiPencil,
mdiPlus,
mdiRefresh,
@@ -302,9 +302,9 @@ class HUIRoot extends LitElement {
? html`
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.menu.assist"
"ui.panel.lovelace.menu.start_conversation"
)}
.path=${mdiCommentProcessingOutline}
.path=${mdiMicrophone}
@click=${this._showVoiceCommandDialog}
></ha-icon-button>
`
@@ -324,7 +324,7 @@ class HUIRoot extends LitElement {
? html`
<mwc-list-item
graphic="icon"
@request-selected=${this._handleShowQuickBar}
@request-selected=${this._showQuickBar}
>
${this.hass!.localize(
"ui.panel.lovelace.menu.search"
@@ -343,15 +343,15 @@ class HUIRoot extends LitElement {
<mwc-list-item
graphic="icon"
@request-selected=${this
._handleShowVoiceCommandDialog}
._showVoiceCommandDialog}
>
${this.hass!.localize(
"ui.panel.lovelace.menu.assist"
"ui.panel.lovelace.menu.start_conversation"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiCommentProcessingOutline}
.path=${mdiMicrophone}
></ha-svg-icon>
</mwc-list-item>
`
@@ -711,13 +711,6 @@ class HUIRoot extends LitElement {
});
}
private _handleShowQuickBar(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._showQuickBar();
}
private _showQuickBar(): void {
showQuickBar(this, {
commandMode: false,
@@ -769,15 +762,6 @@ class HUIRoot extends LitElement {
navigate(`${this.route?.prefix}/hass-unused-entities`);
}
private _handleShowVoiceCommandDialog(
ev: CustomEvent<RequestSelectedDetail>
): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._showVoiceCommandDialog();
}
private _showVoiceCommandDialog(): void {
showVoiceCommandDialog(this);
}

View File

@@ -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";

View File

@@ -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);

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -803,15 +803,13 @@
"nothing_found": "Nothing found!"
},
"voice_command": {
"title": "Assistant",
"did_not_hear": "Home Assistant did not hear anything",
"did_not_understand": "Didn't quite get that",
"found": "I found the following for you:",
"error": "Oops, an error has occurred",
"how_can_i_help": "How can I assist?",
"input_label": "Enter a request",
"send_text": "Send text",
"start_listening": "Start listening"
"how_can_i_help": "How can I help?",
"label": "Type a question and press 'Enter'",
"label_voice": "Type and press 'Enter' or tap the microphone to speak"
},
"generic": {
"cancel": "Cancel",
@@ -1035,8 +1033,7 @@
"mode": "Display mode",
"text": "Text",
"password": "Password",
"pattern_label": "Regex pattern",
"pattern_helper": "Used for client-side validation"
"pattern": "Regex pattern for client-side validation"
},
"input_number": {
"min": "Minimum value",
@@ -1636,6 +1633,66 @@
"device_consumption_energy": "Device consumption energy (kWh)",
"selected_stat_intro": "Select the entity that represents the device energy usage."
}
},
"validation": {
"issues": {
"entity_not_defined": {
"title": "Entity not defined",
"description": "Check the integration or your configuration that provides:"
},
"recorder_untracked": {
"title": "Entity not tracked",
"description": "The recorder has been configured to exclude these configured entities:"
},
"entity_unavailable": {
"title": "Entity unavailable",
"description": "The state of these configured entities are currently not available:"
},
"entity_state_non_numeric": {
"title": "Entity has non-numeric state",
"description": "The following entities have a state that cannot be parsed as a number:"
},
"entity_negative_state": {
"title": "Entity has a negative state",
"description": "The following entities have a negative state while a positive state is expected:"
},
"entity_unexpected_unit_energy": {
"title": "Unexpected unit of measurement",
"description": "The following entities do not have the expected units of measurement 'Wh', 'kWh', 'MWh' or 'GJ':"
},
"entity_unexpected_unit_gas": {
"title": "Unexpected unit of measurement",
"description": "The following entities do not have the expected units of measurement 'Wh', 'kWh', 'MWh' or 'GJ' for an energy sensor or 'm³' or 'ft³' for a gas sensor:"
},
"entity_unexpected_unit_water": {
"title": "Unexpected unit of measurement",
"description": "The following entities do not have the expected units of measurement 'm³' or 'ft³' for a water sensor:"
},
"entity_unexpected_unit_energy_price": {
"title": "Unexpected unit of measurement",
"description": "The following entities do not have the expected units of measurement ''{currency}/kWh'', ''{currency}/Wh'', ''{currency}/MWh'' or ''{currency}/GJ'':"
},
"entity_unexpected_unit_gas_price": {
"title": "Unexpected unit of measurement",
"description": "The following entities do not have the expected units of measurement ''{currency}/kWh'', ''{currency}/Wh'', ''{currency}/MWh'', ''{currency}/GJ'', ''{currency}/m³'' or ''{currency}/ft³'':"
},
"entity_unexpected_unit_water_price": {
"title": "Unexpected unit of measurement",
"description": "The following entities do not have the expected units of measurement ''{currency}/m³'' or ''{currency}/ft³'':"
},
"entity_unexpected_state_class": {
"title": "Unexpected state class",
"description": "The following entities do not have the expected state class:"
},
"entity_unexpected_device_class": {
"title": "Unexpected device class",
"description": "The following entities do not have the expected device class:"
},
"entity_state_class_measurement_no_last_reset": {
"title": "Last reset missing",
"description": "The following entities have state class 'measurement' but 'last_reset' is missing:"
}
}
}
},
"helpers": {
@@ -2683,10 +2740,6 @@
"exposed": "{selected} exposed",
"not_exposed": "{selected} not exposed",
"manage_aliases": "Manage aliases",
"add_aliases": "Add aliases",
"no_aliases": "No aliases",
"aliases_not_available": "Aliases not available",
"aliases_not_available_learn_more": "Learn more",
"sync_to_google": "Synchronizing changes to Google.",
"sync_entities": "Synchronize entities",
"sync_entities_error": "Failed to sync entities:",
@@ -3858,7 +3911,7 @@
"configure_ui": "Edit Dashboard",
"help": "Help",
"search": "Search",
"assist": "Assist",
"start_conversation": "Start conversation",
"reload_resources": "Reload resources",
"exit_edit_mode": "Done",
"close": "Close"

3106
yarn.lock

File diff suppressed because it is too large Load Diff