Compare commits

..

2 Commits

Author SHA1 Message Date
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
97 changed files with 2648 additions and 3215 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

@@ -71,6 +71,7 @@ class HaDemo extends HomeAssistantAppEl {
entity_category: null,
has_entity_name: false,
unique_id: "co2_intensity",
aliases: [],
},
{
config_entry_id: "co2signal",
@@ -86,6 +87,7 @@ class HaDemo extends HomeAssistantAppEl {
entity_category: null,
has_entity_name: false,
unique_id: "grid_fossil_fuel_percentage",
aliases: [],
},
]);

View File

@@ -197,6 +197,7 @@ const createEntityRegistryEntries = (
platform: "updater",
has_entity_name: false,
unique_id: "updater",
aliases: [],
},
];

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 = "20221230.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

@@ -157,7 +157,7 @@ export const CURRENCIES = [
"XPF",
"YER",
"ZAR",
"ZMW",
"ZMK",
"ZWL",
];

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

@@ -22,7 +22,6 @@ import {
isNumericState,
} from "../../common/number/format_number";
import { isUnavailableState, UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { timerTimeRemaining } from "../../data/timer";
import { HomeAssistant } from "../../types";
import "../ha-label-badge";
@@ -35,9 +34,9 @@ const TRUNCATED_DOMAINS = [
"person",
] as const satisfies ReadonlyArray<keyof typeof FIXED_DOMAIN_STATES>;
type TruncatedDomain = (typeof TRUNCATED_DOMAINS)[number];
type TruncatedDomain = typeof TRUNCATED_DOMAINS[number];
type TruncatedKey = {
[T in TruncatedDomain]: `${T}.${(typeof FIXED_DOMAIN_STATES)[T][number]}`;
[T in TruncatedDomain]: `${T}.${typeof FIXED_DOMAIN_STATES[T][number]}`;
}[TruncatedDomain];
const getTruncatedKey = (domainKey: string, stateKey: string) => {
@@ -104,10 +103,8 @@ export class HaStateLabelBadge extends LitElement {
// 4. Icon determined via entity state
// 5. Value string as fallback
const domain = computeStateDomain(entityState);
const entry = this.hass?.entities[entityState.entity_id];
const showIcon =
this.icon || this._computeShowIcon(domain, entityState, entry);
const showIcon = this.icon || this._computeShowIcon(domain, entityState);
const image = this.icon
? ""
: this.image
@@ -115,9 +112,7 @@ export class HaStateLabelBadge extends LitElement {
: entityState.attributes.entity_picture_local ||
entityState.attributes.entity_picture;
const value =
!image && !showIcon
? this._computeValue(domain, entityState, entry)
: undefined;
!image && !showIcon ? this._computeValue(domain, entityState) : undefined;
return html`
<ha-label-badge
@@ -157,11 +152,7 @@ export class HaStateLabelBadge extends LitElement {
}
}
private _computeValue(
domain: string,
entityState: HassEntity,
entry?: EntityRegistryEntry
) {
private _computeValue(domain: string, entityState: HassEntity) {
switch (domain) {
case "alarm_control_panel":
case "binary_sensor":
@@ -174,7 +165,7 @@ export class HaStateLabelBadge extends LitElement {
return null;
// @ts-expect-error we don't break and go to default
case "sensor":
if (entry?.platform === "moon") {
if (entityState.attributes.device_class === "moon__phase") {
return null;
}
// eslint-disable-next-line: disable=no-fallthrough
@@ -197,11 +188,7 @@ export class HaStateLabelBadge extends LitElement {
}
}
private _computeShowIcon(
domain: string,
entityState: HassEntity,
entry?: EntityRegistryEntry
): boolean {
private _computeShowIcon(domain: string, entityState: HassEntity): boolean {
if (entityState.state === UNAVAILABLE) {
return false;
}
@@ -217,7 +204,7 @@ export class HaStateLabelBadge extends LitElement {
case "timer":
return true;
case "sensor":
return entry?.platform === "moon";
return entityState.attributes.device_class === "moon__phase";
default:
return false;
}
@@ -236,10 +223,6 @@ export class HaStateLabelBadge extends LitElement {
if (domainStateKey) {
return this.hass!.localize(`state_badge.${domainStateKey}`);
}
// Person and device tracker state can be zone name
if (domain === "person" || domain === "device_tracker") {
return entityState.state;
}
if (domain === "timer") {
return secondsToDuration(_timerTimeRemaining);
}

View File

@@ -28,7 +28,7 @@ class StateInfo extends LitElement {
const name = computeStateName(this.stateObj);
return html`<state-badge
return html` <state-badge
.stateObj=${this.stateObj}
.stateColor=${true}
.color=${this.color}

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

@@ -3,12 +3,10 @@ import { styles } from "@material/mwc-dialog/mwc-dialog.css";
import { mdiClose } from "@mdi/js";
import { css, html, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
import type { HomeAssistant } from "../types";
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
import "./ha-icon-button";
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button"];
export const createCloseHeading = (
hass: HomeAssistant,
title: string | TemplateResult
@@ -34,14 +32,6 @@ export class HaDialog extends DialogBase {
return html`<slot name="heading"> ${super.renderHeading()} </slot>`;
}
protected firstUpdated(): void {
super.firstUpdated();
this.suppressDefaultPressSelector = [
this.suppressDefaultPressSelector,
SUPPRESS_DEFAULT_PRESS_SELECTOR,
].join(", ");
}
static override styles = [
styles,
css`

View File

@@ -67,9 +67,6 @@ export class HaFormInteger extends LitElement implements HaFormElement {
@change=${this._valueChanged}
></ha-slider>
</div>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
</div>
`;
}

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

@@ -9,7 +9,6 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import {
DeviceRegistryEntry,
getDeviceIntegrationLookup,
@@ -79,7 +78,7 @@ export class HaTargetSelector extends LitElement {
? [this.selector.target?.entity.device_class]
: undefined}
.includeDomains=${this.selector.target?.entity?.domain
? ensureArray(this.selector.target.entity.domain as string | string[])
? [this.selector.target?.entity.domain]
: undefined}
.disabled=${this.disabled}
></ha-target-picker>`;

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

@@ -22,6 +22,7 @@ export interface EntityRegistryEntry {
original_name?: string;
unique_id: string;
translation_key?: string;
aliases: string[];
}
export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
@@ -29,7 +30,6 @@ export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
original_icon?: string;
device_class?: string;
original_device_class?: string;
aliases: string[];
}
export interface UpdateEntityRegistryEntryResult {
@@ -111,15 +111,6 @@ export const getExtendedEntityRegistryEntry = (
entity_id: entityId,
});
export const getExtendedEntityRegistryEntries = (
hass: HomeAssistant,
entityIds: string[]
): Promise<Record<string, ExtEntityRegistryEntry>> =>
hass.callWS({
type: "config/entity_registry/get_entries",
entity_ids: entityIds,
});
export const updateEntityRegistryEntry = (
hass: HomeAssistant,
entityId: string,

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

@@ -51,7 +51,7 @@ class DialogCalendarEventEditor extends LitElement {
@state() private _summary = "";
@state() private _description? = "";
@state() private _description = "";
@state() private _rrule?: string;
@@ -87,7 +87,6 @@ class DialogCalendarEventEditor extends LitElement {
const entry = params.entry!;
this._allDay = isDate(entry.dtstart);
this._summary = entry.summary;
this._description = entry.description;
this._rrule = entry.rrule;
if (this._allDay) {
this._dtstart = new Date(entry.dtstart + "T00:00:00");

View File

@@ -1,5 +1,4 @@
import type { SelectedDetail } from "@material/mwc-list";
import { formatInTimeZone, toDate } from "date-fns-tz";
import { css, html, LitElement, PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -7,7 +6,6 @@ import type { Options, WeekdayStr } from "rrule";
import { ByWeekday, RRule, Weekday } from "rrule";
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { LocalizeKeys } from "../../common/translations/localize";
import "../../components/ha-chip";
import "../../components/ha-list-item";
import "../../components/ha-select";
@@ -21,10 +19,12 @@ import {
getWeekday,
getWeekdays,
getMonthlyRepeatItems,
intervalSuffix,
RepeatEnd,
RepeatFrequency,
ruleByWeekDay,
untilValue,
WEEKDAY_NAME,
MonthlyRepeatItem,
getMonthlyRepeatWeekdayFromRule,
getMonthdayRepeatFromRule,
@@ -65,7 +65,7 @@ export class RecurrenceRuleEditor extends LitElement {
@state() private _count?: number;
@state() private _untilDay?: Date;
@state() private _until?: Date;
@query("#monthly") private _monthlyRepeatSelect!: HaSelect;
@@ -98,17 +98,15 @@ export class RecurrenceRuleEditor extends LitElement {
}
if (
!changedProps.has("value") &&
(changedProps.has("dtstart") ||
changedProps.has("timezone") ||
changedProps.has("_freq") ||
changedProps.has("_interval") ||
changedProps.has("_weekday") ||
changedProps.has("_monthlyRepeatWeekday") ||
changedProps.has("_monthday") ||
changedProps.has("_end") ||
changedProps.has("_count") ||
changedProps.has("_untilDay"))
changedProps.has("timezone") ||
changedProps.has("_freq") ||
changedProps.has("_interval") ||
changedProps.has("_weekday") ||
changedProps.has("_monthlyRepeatWeekday") ||
changedProps.has("_monthday") ||
changedProps.has("_end") ||
changedProps.has("_count") ||
changedProps.has("_until")
) {
this._updateRule();
return;
@@ -125,7 +123,7 @@ export class RecurrenceRuleEditor extends LitElement {
this._monthlyRepeatWeekday = undefined;
this._end = "never";
this._count = undefined;
this._untilDay = undefined;
this._until = undefined;
this._computedRRule = this.value;
if (this.value === "") {
@@ -165,7 +163,7 @@ export class RecurrenceRuleEditor extends LitElement {
}
if (rrule.until) {
this._end = "on";
this._untilDay = toDate(rrule.until, { timeZone: this.timezone });
this._until = rrule.until;
} else if (rrule.count) {
this._end = "after";
this._count = rrule.count;
@@ -176,36 +174,18 @@ export class RecurrenceRuleEditor extends LitElement {
return html`
<ha-select
id="freq"
label=${this.hass.localize("ui.components.calendar.event.repeat.label")}
label="Repeat"
@selected=${this._onRepeatSelected}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._freq}
>
<ha-list-item value="none">
${this.hass.localize("ui.components.calendar.event.repeat.freq.none")}
</ha-list-item>
<ha-list-item value="yearly">
${this.hass.localize(
"ui.components.calendar.event.repeat.freq.yearly"
)}
</ha-list-item>
<ha-list-item value="monthly">
${this.hass.localize(
"ui.components.calendar.event.repeat.freq.monthly"
)}
</ha-list-item>
<ha-list-item value="weekly">
${this.hass.localize(
"ui.components.calendar.event.repeat.freq.weekly"
)}
</ha-list-item>
<ha-list-item value="daily">
${this.hass.localize(
"ui.components.calendar.event.repeat.freq.daily"
)}
</ha-list-item>
<ha-list-item value="none">None</ha-list-item>
<ha-list-item value="yearly">Yearly</ha-list-item>
<ha-list-item value="monthly">Monthly</ha-list-item>
<ha-list-item value="weekly">Weekly</ha-list-item>
<ha-list-item value="daily">Daily</ha-list-item>
</ha-select>
`;
}
@@ -216,9 +196,7 @@ export class RecurrenceRuleEditor extends LitElement {
${this._monthlyRepeatItems.length > 0
? html`<ha-select
id="monthly"
label=${this.hass.localize(
"ui.components.calendar.event.repeat.monthly.label"
)}
label="Repeat Monthly"
@selected=${this._onMonthlyDetailSelected}
.value=${this._monthlyRepeat || this._monthlyRepeatItems[0]?.value}
@closed=${stopPropagation}
@@ -247,11 +225,7 @@ export class RecurrenceRuleEditor extends LitElement {
.value=${item}
class=${classMap({ active: this._weekday.has(item) })}
@click=${this._onWeekdayToggle}
>${this.hass.localize(
`ui.components.calendar.event.repeat.weekly.weekday.${
item.toLowerCase() as Lowercase<WeekdayStr>
}`
)}</ha-chip
>${WEEKDAY_NAME[item]}</ha-chip
>
`
)}
@@ -267,16 +241,11 @@ export class RecurrenceRuleEditor extends LitElement {
return html`
<ha-textfield
id="interval"
label=${this.hass.localize(
"ui.components.calendar.event.repeat.interval.label"
)}
label="Repeat interval"
type="number"
min="1"
.value=${this._interval}
.suffix=${this.hass.localize(
`ui.components.calendar.event.repeat.interval.${this
._freq!}` as LocalizeKeys
)}
.suffix=${intervalSuffix(this._freq!)}
@change=${this._onIntervalChange}
></ha-textfield>
`;
@@ -286,38 +255,26 @@ export class RecurrenceRuleEditor extends LitElement {
return html`
<ha-select
id="end"
label=${this.hass.localize(
"ui.components.calendar.event.repeat.end.label"
)}
label="Ends"
.value=${this._end}
@selected=${this._onEndSelected}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
<ha-list-item value="never">
${this.hass.localize("ui.components.calendar.event.repeat.end.never")}
</ha-list-item>
<ha-list-item value="after">
${this.hass.localize("ui.components.calendar.event.repeat.end.after")}
</ha-list-item>
<ha-list-item value="on">
${this.hass.localize("ui.components.calendar.event.repeat.end.on")}
</ha-list-item>
<ha-list-item value="never">Never</ha-list-item>
<ha-list-item value="after">After</ha-list-item>
<ha-list-item value="on">On</ha-list-item>
</ha-select>
${this._end === "after"
? html`
<ha-textfield
id="after"
label=${this.hass.localize(
"ui.components.calendar.event.repeat.end_after.label"
)}
label="End after"
type="number"
min="1"
.value=${this._count!}
suffix=${this.hass.localize(
"ui.components.calendar.event.repeat.end_after.ocurrences"
)}
suffix="ocurrences"
@change=${this._onCountChange}
></ha-textfield>
`
@@ -326,11 +283,9 @@ export class RecurrenceRuleEditor extends LitElement {
? html`
<ha-date-input
id="on"
label=${this.hass.localize(
"ui.components.calendar.event.repeat.end_on.label"
)}
label="End on"
.locale=${this.locale}
.value=${this._formatDate(this._untilDay!)}
.value=${this._until!.toISOString()}
@value-changed=${this._onUntilChange}
></ha-date-input>
`
@@ -384,7 +339,6 @@ export class RecurrenceRuleEditor extends LitElement {
} else {
this._weekday.delete(value);
}
this.requestUpdate("_weekday");
}
private _onEndSelected(e: CustomEvent<SelectedDetail<number>>) {
@@ -397,15 +351,15 @@ export class RecurrenceRuleEditor extends LitElement {
switch (this._end) {
case "after":
this._count = DEFAULT_COUNT[this._freq!];
this._untilDay = undefined;
this._until = undefined;
break;
case "on":
this._count = undefined;
this._untilDay = untilValue(this._freq!);
this._until = untilValue(this._freq!);
break;
default:
this._count = undefined;
this._untilDay = undefined;
this._until = undefined;
}
e.stopPropagation();
}
@@ -416,9 +370,7 @@ export class RecurrenceRuleEditor extends LitElement {
private _onUntilChange(e: CustomEvent) {
e.stopPropagation();
this._untilDay = toDate(e.detail.value + "T00:00:00", {
timeZone: this.timezone,
});
this._until = new Date(e.detail.value);
}
// Reset the weekday selected when there is only a single value
@@ -447,27 +399,18 @@ export class RecurrenceRuleEditor extends LitElement {
freq: convertRepeatFrequency(this._freq!)!,
interval: this._interval > 1 ? this._interval : undefined,
count: this._count,
until: this._until,
tzid: this.timezone,
byweekday: byweekday,
bymonthday: bymonthday,
};
let contentline = RRule.optionsToString(options);
if (this._untilDay) {
// The UNTIL value should be inclusive of the last event instance
const until = toDate(
this._formatDate(this._untilDay!) +
"T" +
this._formatTime(this.dtstart!),
{ timeZone: this.timezone }
);
// rrule.js can't compute some UNTIL variations so we compute that ourself. Must be
// in the same format as dtstart.
const format = this.allDay ? "yyyyMMdd" : "yyyyMMdd'T'HHmmss";
const newUntilValue = formatInTimeZone(
until,
this.hass.config.time_zone,
format
);
contentline += `;UNTIL=${newUntilValue}`;
if (this._until && this.allDay) {
// rrule.js only computes UNTIL values as DATE-TIME however rfc5545 says
// The value of the UNTIL rule part MUST have the same value type as the
// "DTSTART" property. If needed, strip off any time values as a workaround
// This converts "UNTIL=20220512T060000" to "UNTIL=20220512"
contentline = contentline.replace(/(UNTIL=\d{8})T\d{6}Z?/, "$1");
}
return contentline.slice(6); // Strip "RRULE:" prefix
}
@@ -487,16 +430,6 @@ export class RecurrenceRuleEditor extends LitElement {
);
}
// Formats a date in browser display timezone
private _formatDate(date: Date): string {
return formatInTimeZone(date, this.timezone!, "yyyy-MM-dd");
}
// Formats a time in browser display timezone
private _formatTime(date: Date): string {
return formatInTimeZone(date, this.timezone!, "HH:mm:ss");
}
static styles = css`
ha-textfield,
ha-select {

View File

@@ -42,6 +42,16 @@ export interface MonthlyRepeatItem {
label: string;
}
export function intervalSuffix(freq: RepeatFrequency) {
if (freq === "monthly") {
return "months";
}
if (freq === "weekly") {
return "weeks";
}
return "days";
}
export function untilValue(freq: RepeatFrequency): Date {
const today = new Date();
const increment = DEFAULT_COUNT[freq];
@@ -92,6 +102,16 @@ export const convertRepeatFrequency = (
}
};
export const WEEKDAY_NAME = {
SU: "Sun",
MO: "Mon",
TU: "Tue",
WE: "Wed",
TH: "Thu",
FR: "Fri",
SA: "Sat",
};
export const WEEKDAYS = [
RRule.SU,
RRule.MO,

View File

@@ -158,7 +158,6 @@ export class HaDeviceAction extends LitElement {
}
ha-form {
display: block;
margin-top: 24px;
}
`;

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

@@ -184,7 +184,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
ev.stopPropagation();
const target = ev.target as any;
const key = target.key;
const value = ev.detail ? ev.detail.value : target.value;
const value = ev.detail?.value ?? target.value;
if (
(this.config.use_blueprint.input &&
this.config.use_blueprint.input[key] === value) ||

View File

@@ -541,10 +541,15 @@ export class HaAutomationTrace extends LitElement {
justify-content: center;
display: flex;
}
.info {
flex: 1;
background-color: var(--card-background-color);
}
.linkButton {
color: var(--primary-text-color);
}
.trace-link {
text-decoration: none;
}

View File

@@ -46,7 +46,7 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
],
],
},
{ name: "offset", selector: { duration: {} } },
{ name: "offset", selector: { duration: { enable_day: true } } },
{
name: "offset_type",
type: "select",

View File

@@ -9,6 +9,7 @@ import {
mdiFormatListChecks,
mdiSync,
} from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -40,17 +41,22 @@ import {
updateCloudAlexaEntityConfig,
updateCloudPref,
} from "../../../../data/cloud";
import { EntityRegistryEntry } from "../../../../data/entity_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../../data/entity_registry";
import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler";
import "../../../../layouts/hass-loading-screen";
import "../../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
const DEFAULT_CONFIG_EXPOSE = true;
const IGNORE_INTERFACES = ["Alexa.EndpointHealth"];
@customElement("cloud-alexa")
class CloudAlexa extends LitElement {
class CloudAlexa extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property()
@@ -162,8 +168,13 @@ class CloudAlexa extends LitElement {
<state-info
.hass=${this.hass}
.stateObj=${stateObj}
secondary-line
@click=${this._showMoreInfo}
>
${entity.interfaces
.filter((ifc) => !IGNORE_INTERFACES.includes(ifc))
.map((ifc) => ifc.replace(/(Alexa.|Controller)/g, ""))
.join(", ")}
</state-info>
${!emptyFilter
? html`${iconButton}`
@@ -312,18 +323,23 @@ class CloudAlexa extends LitElement {
if (changedProps.has("cloudStatus")) {
this._entityConfigs = this.cloudStatus.prefs.alexa_entity_configs;
}
if (
changedProps.has("hass") &&
changedProps.get("hass")?.entities !== this.hass.entities
) {
const categories = {};
}
for (const entry of Object.values(this.hass.entities)) {
categories[entry.entity_id] = entry.entity_category;
}
protected override hassSubscribe(): (
| UnsubscribeFunc
| Promise<UnsubscribeFunc>
)[] {
return [
subscribeEntityRegistry(this.hass.connection, (entries) => {
const categories = {};
this._entityCategories = categories;
}
for (const entry of entries) {
categories[entry.entity_id] = entry.entity_category;
}
this._entityCategories = categories;
}),
];
}
private async _fetchData() {
@@ -526,7 +542,6 @@ class CloudAlexa extends LitElement {
}
state-info {
cursor: pointer;
height: 40px;
}
ha-switch {
padding: 8px 0;

View File

@@ -9,6 +9,7 @@ import {
mdiFormatListChecks,
mdiSync,
} from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -40,9 +41,7 @@ import {
} from "../../../../data/cloud";
import {
EntityRegistryEntry,
ExtEntityRegistryEntry,
getExtendedEntityRegistryEntries,
updateEntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../../data/entity_registry";
import {
fetchCloudGoogleEntities,
@@ -52,15 +51,15 @@ import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
import "../../../../layouts/hass-loading-screen";
import "../../../../layouts/hass-subpage";
import { buttonLinkStyle, haStyle } from "../../../../resources/styles";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast";
import { showEntityAliasesDialog } from "../../entities/entity-aliases/show-dialog-entity-aliases";
const DEFAULT_CONFIG_EXPOSE = true;
@customElement("cloud-google-assistant")
class CloudGoogleAssistant extends LitElement {
class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public cloudStatus!: CloudStatusLoggedIn;
@@ -69,8 +68,6 @@ class CloudGoogleAssistant extends LitElement {
@state() private _entities?: GoogleEntity[];
@state() private _entries?: { [id: string]: ExtEntityRegistryEntry };
@state() private _syncing = false;
@state()
@@ -167,8 +164,6 @@ class CloudGoogleAssistant extends LitElement {
: mdiCloseBoxMultiple}
></ha-icon-button>`;
const aliases = this._entries?.[entity.entity_id]?.aliases;
target.push(html`
<ha-card outlined>
<div class="card-content">
@@ -179,57 +174,15 @@ class CloudGoogleAssistant extends LitElement {
secondary-line
@click=${this._showMoreInfo}
>
${aliases
? html`
<span>
${aliases.length > 0
? [...aliases]
.sort((a, b) =>
stringCompare(a, b, this.hass.locale.language)
)
.join(", ")
: this.hass.localize(
"ui.panel.config.cloud.google.no_aliases"
)}
</span>
<br />
<button
class="link"
.entityId=${entity.entity_id}
@click=${this._openAliasesSettings}
>
${this.hass.localize(
`ui.panel.config.cloud.google.${
aliases.length > 0
? "manage_aliases"
: "add_aliases"
}`
)}
</button>
`
: html`
<span>
${this.hass.localize(
"ui.panel.config.cloud.google.aliases_not_available"
)}
</span>
<br />
<button
class="link"
.stateObj=${stateObj}
@click=${this._showMoreInfoSettings}
>
${this.hass.localize(
"ui.panel.config.cloud.google.aliases_not_available_learn_more"
)}
</button>
`}
${entity.traits
.map((trait) => trait.substr(trait.lastIndexOf(".") + 1))
.join(", ")}
</state-info>
${!emptyFilter
? html`${iconButton}`
: html`<ha-button-menu
corner="BOTTOM_START"
.entityId=${entity.entity_id}
.entityId=${stateObj.entity_id}
@action=${this._exposeChanged}
>
${iconButton}
@@ -355,7 +308,7 @@ class CloudGoogleAssistant extends LitElement {
</h3>
${!this.narrow
? this.hass!.localize(
"ui.panel.config.cloud.google.exposed",
"ui.panel.config.cloud.alexa.exposed",
"selected",
selected
)
@@ -376,7 +329,7 @@ class CloudGoogleAssistant extends LitElement {
</h3>
${!this.narrow
? this.hass!.localize(
"ui.panel.config.cloud.google.not_exposed",
"ui.panel.config.cloud.alexa.not_exposed",
"selected",
this._entities.length - selected
)
@@ -401,38 +354,23 @@ class CloudGoogleAssistant extends LitElement {
if (changedProps.has("cloudStatus")) {
this._entityConfigs = this.cloudStatus.prefs.google_entity_configs;
}
if (
changedProps.has("hass") &&
changedProps.get("hass")?.entities !== this.hass.entities
) {
const categories = {};
for (const entry of Object.values(this.hass.entities)) {
categories[entry.entity_id] = entry.entity_category;
}
this._entityCategories = categories;
}
}
private async _openAliasesSettings(ev) {
ev.stopPropagation();
const entityId = ev.target.entityId;
const entry = this._entries![entityId];
if (!entry) {
return;
}
showEntityAliasesDialog(this, {
entity: entry,
updateEntry: async (updates) => {
const { entity_entry } = await updateEntityRegistryEntry(
this.hass,
entry.entity_id,
updates
);
this._entries![entity_entry.entity_id] = entity_entry;
},
});
protected override hassSubscribe(): (
| UnsubscribeFunc
| Promise<UnsubscribeFunc>
)[] {
return [
subscribeEntityRegistry(this.hass.connection, (entries) => {
const categories = {};
for (const entry of entries) {
categories[entry.entity_id] = entry.entity_category;
}
this._entityCategories = categories;
}),
];
}
private _configIsDomainExposed(
@@ -459,13 +397,6 @@ class CloudGoogleAssistant extends LitElement {
private async _fetchData() {
const entities = await fetchCloudGoogleEntities(this.hass);
this._entries = await getExtendedEntityRegistryEntries(
this.hass,
entities
.filter((ent) => this.hass.entities[ent.entity_id])
.map((e) => e.entity_id)
);
entities.sort((a, b) => {
const stateA = this.hass.states[a.entity_id];
const stateB = this.hass.states[b.entity_id];
@@ -480,14 +411,7 @@ class CloudGoogleAssistant extends LitElement {
private _showMoreInfo(ev) {
const entityId = ev.currentTarget.stateObj.entity_id;
const moreInfoTab = ev.currentTarget.moreInfoTab;
fireEvent(this, "hass-more-info", { entityId, tab: moreInfoTab });
}
private _showMoreInfoSettings(ev) {
ev.stopPropagation();
const entityId = ev.currentTarget.stateObj.entity_id;
fireEvent(this, "hass-more-info", { entityId, tab: "settings" });
fireEvent(this, "hass-more-info", { entityId });
}
private async _exposeChanged(ev: CustomEvent<ActionDetail>) {
@@ -659,7 +583,6 @@ class CloudGoogleAssistant extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyle,
buttonLinkStyle,
css`
mwc-list-item > [slot="meta"] {
margin-left: 4px;

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

@@ -57,13 +57,7 @@ export class EntityRegistrySettingsHelper extends LitElement {
super.updated(changedProperties);
if (changedProperties.has("entry")) {
this._error = undefined;
if (
this.entry.unique_id !==
(changedProperties.get("entry") as ExtEntityRegistryEntry)?.unique_id
) {
this._item = undefined;
}
this._item = undefined;
this._getItem();
}
}

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;
}
@@ -72,21 +72,16 @@ class DialogEntityAliases extends LitElement {
dialogInitialFocus=${index}
.index=${index}
class="flex-auto"
.label=${this.hass!.localize(
"ui.dialogs.entity_registry.editor.aliases.input_label",
{ number: index + 1 }
)}
label="Alias"
.value=${alias}
?data-last=${index === this._aliases.length - 1}
@input=${this._editAlias}
@keydown=${this._keyDownAlias}
@change=${this._editAlias}
></ha-textfield>
<ha-icon-button
.index=${index}
slot="navigationIcon"
label=${this.hass!.localize(
"ui.dialogs.entity_registry.editor.aliases.remove_alias",
{ number: index + 1 }
"ui.dialogs.entity_registry.editor.aliases.remove_alias"
)}
@click=${this._removeAlias}
.path=${mdiDeleteOutline}
@@ -138,13 +133,6 @@ class DialogEntityAliases extends LitElement {
this._aliases[index] = (ev.target as any).value;
}
private async _keyDownAlias(ev: KeyboardEvent) {
if (ev.key === "Enter") {
ev.stopPropagation();
this._addAlias();
}
}
private async _removeAlias(ev: Event) {
const index = (ev.target as any).index;
const aliases = [...this._aliases];

View File

@@ -1,11 +1,11 @@
import { fireEvent } from "../../../../common/dom/fire_event";
import {
EntityRegistryEntry,
EntityRegistryEntryUpdateParams,
ExtEntityRegistryEntry,
} from "../../../../data/entity_registry";
export interface EntityAliasesDialogParams {
entity: ExtEntityRegistryEntry;
entity: EntityRegistryEntry;
updateEntry: (
updates: Partial<EntityRegistryEntryUpdateParams>
) => Promise<unknown>;

View File

@@ -1,13 +1,8 @@
import "@material/mwc-formfield/mwc-formfield";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiPencil } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stringCompare } from "../../../common/string/compare";
import "../../../components/ha-area-picker";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-radio";
@@ -26,7 +21,6 @@ import {
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../types";
import { showEntityAliasesDialog } from "./entity-aliases/show-dialog-entity-aliases";
@customElement("ha-registry-basic-editor")
export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
@@ -50,21 +44,6 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
@state() private _submitting = false;
private _handleAliasesClicked(ev: CustomEvent) {
if (ev.detail.index !== 0) return;
showEntityAliasesDialog(this, {
entity: this.entry!,
updateEntry: async (updates) => {
const result = await updateEntityRegistryEntry(
this.hass,
this.entry.entity_id,
updates
);
fireEvent(this, "entity-entry-updated", result.entity_entry);
},
});
}
public async updateEntry(): Promise<void> {
this._submitting = true;
const params: Partial<EntityRegistryEntryUpdateParams> = {
@@ -268,37 +247,6 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
</div>
`
: ""}
<div class="label">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.aliases_section"
)}
</div>
<mwc-list class="aliases" @action=${this._handleAliasesClicked}>
<mwc-list-item .twoline=${this.entry.aliases.length > 0} hasMeta>
<span>
${this.entry.aliases.length > 0
? this.hass.localize(
"ui.dialogs.entity_registry.editor.configured_aliases",
{ count: this.entry.aliases.length }
)
: this.hass.localize(
"ui.dialogs.entity_registry.editor.no_aliases"
)}
</span>
<span slot="secondary">
${[...this.entry.aliases]
.sort((a, b) => stringCompare(a, b, this.hass.locale.language))
.join(", ")}
</span>
<ha-svg-icon slot="meta" .path=${mdiPencil}></ha-svg-icon>
</mwc-list-item>
</mwc-list>
<div class="secondary">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.aliases.description"
)}
</div>
</ha-expansion-panel>
`;
}
@@ -352,13 +300,6 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
.label {
margin-top: 16px;
}
.aliases {
border-radius: 4px;
margin-top: 4px;
margin-bottom: 4px;
--mdc-icon-button-size: 24px;
overflow: hidden;
}
`;
}
}

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,22 @@ const OVERRIDE_NUMBER_UNITS = {
temperature: ["°C", "°F", "K"],
};
const OVERRIDE_SENSOR_UNITS = {
current: ["A", "mA"],
distance: ["cm", "ft", "in", "km", "m", "mi", "mm", "yd"],
gas: ["CCF", "ft³", "m³"],
precipitation: ["cm", "in", "mm"],
precipitation_intensity: ["in/d", "in/h", "mm/d", "mm/h"],
pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"],
speed: ["ft/s", "in/d", "in/h", "km/h", "kn", "m/s", "mm/d", "mm/h", "mph"],
temperature: ["°C", "°F", "K"],
voltage: ["V", "mV"],
volume: ["CCF", "fl. oz.", "ft³", "gal", "L", "mL", "m³"],
water: ["CCF", "ft³", "gal", "L", "m³"],
weight: ["g", "kg", "lb", "mg", "oz", "st", "µg"],
wind_speed: ["ft/s", "km/h", "kn", "mph", "m/s"],
};
const OVERRIDE_WEATHER_UNITS = {
precipitation: ["mm", "in"],
pressure: ["hPa", "mbar", "mmHg", "inHg"],
@@ -172,8 +187,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@state() private _cameraPrefs?: CameraPreferences;
@state() private _sensorDeviceClassConvertibleUnits?: string[];
private _origEntityId!: string;
private _deviceLookup?: Record<string, DeviceRegistryEntry>;
@@ -277,22 +290,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
}
}
protected async updated(changedProps: PropertyValues): Promise<void> {
if (changedProps.has("_deviceClass")) {
const domain = computeDomain(this.entry.entity_id);
if (domain === "sensor" && this._deviceClass) {
const { units } = await getSensorDeviceClassConvertibleUnits(
this.hass,
this._deviceClass
);
this._sensorDeviceClassConvertibleUnits = units;
} else {
this._sensorDeviceClassConvertibleUnits = [];
}
}
}
protected render(): TemplateResult {
if (this.entry.entity_id !== this._origEntityId) {
return html``;
@@ -437,7 +434,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
${domain === "sensor" &&
this._deviceClass &&
stateObj?.attributes.unit_of_measurement &&
this._sensorDeviceClassConvertibleUnits?.includes(
OVERRIDE_SENSOR_UNITS[this._deviceClass]?.includes(
stateObj?.attributes.unit_of_measurement
)
? html`
@@ -451,7 +448,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@selected=${this._unitChanged}
@closed=${stopPropagation}
>
${this._sensorDeviceClassConvertibleUnits.map(
${OVERRIDE_SENSOR_UNITS[this._deviceClass].map(
(unit: string) => html`
<mwc-list-item .value=${unit}>${unit}</mwc-list-item>
`
@@ -774,8 +771,12 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
"ui.dialogs.entity_registry.editor.aliases_section"
)}
</div>
<mwc-list class="aliases" @action=${this._handleAliasesClicked}>
<mwc-list-item .twoline=${this.entry.aliases.length > 0} hasMeta>
<mwc-list class="aliases">
<mwc-list-item
.twoline=${this.entry.aliases.length > 0}
hasMeta
@click=${this._openAliasesSettings}
>
<span>
${this.entry.aliases.length > 0
? this.hass.localize(
@@ -786,13 +787,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
"ui.dialogs.entity_registry.editor.no_aliases"
)}
</span>
<span slot="secondary">
${[...this.entry.aliases]
.sort((a, b) =>
stringCompare(a, b, this.hass.locale.language)
)
.join(", ")}
</span>
<span slot="secondary">${this.entry.aliases.join(", ")}</span>
<ha-svg-icon slot="meta" .path=${mdiPencil}></ha-svg-icon>
</mwc-list-item>
</mwc-list>
@@ -984,8 +979,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
});
}
private _handleAliasesClicked(ev: CustomEvent) {
if (ev.detail.index !== 0) return;
private _openAliasesSettings() {
showEntityAliasesDialog(this, {
entity: this.entry!,
updateEntry: async (updates) => {

View File

@@ -728,6 +728,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
selectable: false,
entity_category: null,
has_entity_name: false,
aliases: [],
});
}
if (changed) {

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

@@ -40,14 +40,10 @@ class HaPanelDevMqtt extends LitElement {
return html`
<hass-subpage .narrow=${this.narrow} .hass=${this.hass}>
<div class="content">
<ha-card
.header=${this.hass.localize("ui.panel.config.mqtt.settings_title")}
>
<ha-card header="MQTT settings">
<div class="card-actions">
<mwc-button @click=${this._openOptionFlow}
>${this.hass.localize(
"ui.panel.config.mqtt.reconfigure"
)}</mwc-button
>Re-configure MQTT</mwc-button
>
</div>
</ha-card>

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

@@ -158,7 +158,7 @@ export class HaBlueprintScriptEditor extends LitElement {
ev.stopPropagation();
const target = ev.target as any;
const key = target.key;
const value = ev.detail ? ev.detail.value : target.value;
const value = ev.detail?.value ?? target.value;
if (
(this.config.use_blueprint.input &&
this.config.use_blueprint.input[key] === value) ||

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

@@ -527,10 +527,15 @@ export class HaScriptTrace extends LitElement {
:host([narrow]) .graph {
max-width: 100%;
}
.info {
flex: 1;
background-color: var(--card-background-color);
}
.linkButton {
color: var(--primary-text-color);
}
.trace-link {
text-decoration: none;
}

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

@@ -59,7 +59,7 @@ const splitByAreaDevice = (
for (const entity of Object.values(entityEntries)) {
const areaId =
entity.area_id ||
(entity.device_id && deviceEntries[entity.device_id]?.area_id);
(entity.device_id && deviceEntries[entity.device_id].area_id);
if (areaId && areaId in areaEntries && entity.entity_id in allEntities) {
if (!(areaId in areasWithEntities)) {
areasWithEntities[areaId] = [];
@@ -161,7 +161,7 @@ export const computeCards = (
renderFooterEntities &&
(domain === "scene" || domain === "script")
) {
const conf: (typeof footerEntities)[0] = {
const conf: typeof footerEntities[0] = {
entity: entityId,
show_icon: true,
show_name: true,

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

@@ -46,7 +46,7 @@ export const handleAction = async (
actionConfig.confirmation &&
(!actionConfig.confirmation.exemptions ||
!actionConfig.confirmation.exemptions.some(
(e) => e.user === hass!.user?.id
(e) => e.user === hass!.user!.id
))
) {
forwardHaptic("warning");

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,
@@ -266,7 +266,7 @@ class HUIRoot extends LitElement {
((Array.isArray(view.visible) &&
!view.visible.some(
(e) =>
e.user === this.hass!.user?.id
e.user === this.hass!.user!.id
)) ||
view.visible === false))
),
@@ -302,9 +302,9 @@ class HUIRoot extends LitElement {
? html`
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.menu.assist"
"ui.panel.lovelace.menu.start_conversation"
)}
.path=${mdiCommentProcessingOutline}
.path=${mdiMicrophone}
@click=${this._showVoiceCommandDialog}
></ha-icon-button>
`
@@ -324,7 +324,7 @@ class HUIRoot extends LitElement {
? html`
<mwc-list-item
graphic="icon"
@request-selected=${this._handleShowQuickBar}
@request-selected=${this._showQuickBar}
>
${this.hass!.localize(
"ui.panel.lovelace.menu.search"
@@ -343,15 +343,15 @@ class HUIRoot extends LitElement {
<mwc-list-item
graphic="icon"
@request-selected=${this
._handleShowVoiceCommandDialog}
._showVoiceCommandDialog}
>
${this.hass!.localize(
"ui.panel.lovelace.menu.assist"
"ui.panel.lovelace.menu.start_conversation"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiCommentProcessingOutline}
.path=${mdiMicrophone}
></ha-svg-icon>
</mwc-list-item>
`
@@ -470,7 +470,7 @@ class HUIRoot extends LitElement {
view.visible !== undefined &&
((Array.isArray(view.visible) &&
!view.visible.some(
(e) => e.user === this.hass!.user?.id
(e) => e.user === this.hass!.user!.id
)) ||
view.visible === false)
),
@@ -711,13 +711,6 @@ class HUIRoot extends LitElement {
});
}
private _handleShowQuickBar(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._showQuickBar();
}
private _showQuickBar(): void {
showQuickBar(this, {
commandMode: false,
@@ -769,15 +762,6 @@ class HUIRoot extends LitElement {
navigate(`${this.route?.prefix}/hass-unused-entities`);
}
private _handleShowVoiceCommandDialog(
ev: CustomEvent<RequestSelectedDetail>
): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._showVoiceCommandDialog();
}
private _showVoiceCommandDialog(): void {
showVoiceCommandDialog(this);
}

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

@@ -644,33 +644,6 @@
"monthly": "months",
"weekly": "weeks",
"daily": "days"
},
"monthly": {
"label": "Repeat Monthly"
},
"weekly": {
"weekday": {
"su": "Sun",
"mo": "Mon",
"tu": "Tue",
"we": "Wed",
"th": "Thu",
"fr": "Fri",
"sa": "Sat"
}
},
"end": {
"label": "End",
"never": "Never",
"after": "After",
"on": "On"
},
"end_on": {
"label": "End On"
},
"end_after": {
"label": "End After",
"ocurrences": "ocurrences"
}
},
"rrule": {
@@ -803,15 +776,13 @@
"nothing_found": "Nothing found!"
},
"voice_command": {
"title": "Assistant",
"did_not_hear": "Home Assistant did not hear anything",
"did_not_understand": "Didn't quite get that",
"found": "I found the following for you:",
"error": "Oops, an error has occurred",
"how_can_i_help": "How can I assist?",
"input_label": "Enter a request",
"send_text": "Send text",
"start_listening": "Start listening"
"how_can_i_help": "How can I help?",
"label": "Type a question and press 'Enter'",
"label_voice": "Type and press 'Enter' or tap the microphone to speak"
},
"generic": {
"cancel": "Cancel",
@@ -1005,8 +976,7 @@
"aliases": {
"heading": "{name} aliases",
"description": "Aliases are alternative names used in voice assistants to refer to this entity.",
"remove_alias": "Remove alias {number}",
"input_label": "Alias {number}",
"remove_alias": "Remove alias",
"save": "Save",
"add_alias": "Add alias",
"no_aliases": "No aliases have been added yet",
@@ -1035,8 +1005,7 @@
"mode": "Display mode",
"text": "Text",
"password": "Password",
"pattern_label": "Regex pattern",
"pattern_helper": "Used for client-side validation"
"pattern": "Regex pattern for client-side validation"
},
"input_number": {
"min": "Minimum value",
@@ -1636,6 +1605,66 @@
"device_consumption_energy": "Device consumption energy (kWh)",
"selected_stat_intro": "Select the entity that represents the device energy usage."
}
},
"validation": {
"issues": {
"entity_not_defined": {
"title": "Entity not defined",
"description": "Check the integration or your configuration that provides:"
},
"recorder_untracked": {
"title": "Entity not tracked",
"description": "The recorder has been configured to exclude these configured entities:"
},
"entity_unavailable": {
"title": "Entity unavailable",
"description": "The state of these configured entities are currently not available:"
},
"entity_state_non_numeric": {
"title": "Entity has non-numeric state",
"description": "The following entities have a state that cannot be parsed as a number:"
},
"entity_negative_state": {
"title": "Entity has a negative state",
"description": "The following entities have a negative state while a positive state is expected:"
},
"entity_unexpected_unit_energy": {
"title": "Unexpected unit of measurement",
"description": "The following entities do not have the expected units of measurement 'Wh', 'kWh', 'MWh' or 'GJ':"
},
"entity_unexpected_unit_gas": {
"title": "Unexpected unit of measurement",
"description": "The following entities do not have the expected units of measurement 'Wh', 'kWh', 'MWh' or 'GJ' for an energy sensor or 'm³' or 'ft³' for a gas sensor:"
},
"entity_unexpected_unit_water": {
"title": "Unexpected unit of measurement",
"description": "The following entities do not have the expected units of measurement 'm³' or 'ft³' for a water sensor:"
},
"entity_unexpected_unit_energy_price": {
"title": "Unexpected unit of measurement",
"description": "The following entities do not have the expected units of measurement ''{currency}/kWh'', ''{currency}/Wh'', ''{currency}/MWh'' or ''{currency}/GJ'':"
},
"entity_unexpected_unit_gas_price": {
"title": "Unexpected unit of measurement",
"description": "The following entities do not have the expected units of measurement ''{currency}/kWh'', ''{currency}/Wh'', ''{currency}/MWh'', ''{currency}/GJ'', ''{currency}/m³'' or ''{currency}/ft³'':"
},
"entity_unexpected_unit_water_price": {
"title": "Unexpected unit of measurement",
"description": "The following entities do not have the expected units of measurement ''{currency}/m³'' or ''{currency}/ft³'':"
},
"entity_unexpected_state_class": {
"title": "Unexpected state class",
"description": "The following entities do not have the expected state class:"
},
"entity_unexpected_device_class": {
"title": "Unexpected device class",
"description": "The following entities do not have the expected device class:"
},
"entity_state_class_measurement_no_last_reset": {
"title": "Last reset missing",
"description": "The following entities have state class 'measurement' but 'last_reset' is missing:"
}
}
}
},
"helpers": {
@@ -2605,7 +2634,7 @@
"enable_state_reporting": "Enable State Reporting",
"info_state_reporting": "If you enable state reporting, Home Assistant will send all state changes of exposed entities to Amazon. This allows you to always see the latest states in the Alexa app and use the state changes to create routines.",
"state_reporting_error": "Unable to {enable_disable} report state.",
"manage_entities": "[%key:ui::panel::config::cloud::account::google::manage_entities%]",
"manage_entities": "Manage Entities",
"enable": "enable",
"disable": "disable",
"not_configured_title": "Alexa is not activated",
@@ -2656,7 +2685,6 @@
"follow_domain": "[%key:ui::panel::config::cloud::google::follow_domain%]",
"exposed": "[%key:ui::panel::config::cloud::google::exposed%]",
"not_exposed": "[%key:ui::panel::config::cloud::google::not_exposed%]",
"manage_aliases": "[%key:ui::panel::config::cloud::google::manage_aliases%]",
"expose": "Expose to Alexa",
"sync_entities": "Synchronize entities",
"sync_entities_error": "Failed to sync entities:"
@@ -2682,11 +2710,6 @@
"follow_domain": "Follow domain",
"exposed": "{selected} exposed",
"not_exposed": "{selected} not exposed",
"manage_aliases": "Manage aliases",
"add_aliases": "Add aliases",
"no_aliases": "No aliases",
"aliases_not_available": "Aliases not available",
"aliases_not_available_learn_more": "Learn more",
"sync_to_google": "Synchronizing changes to Google.",
"sync_entities": "Synchronize entities",
"sync_entities_error": "Failed to sync entities:",
@@ -3192,8 +3215,6 @@
},
"mqtt": {
"title": "MQTT",
"settings_title": "MQTT settings",
"reconfigure": "Re-configure MQTT",
"description_publish": "Publish a packet",
"topic": "Topic",
"payload": "Payload (template allowed)",
@@ -3858,7 +3879,7 @@
"configure_ui": "Edit Dashboard",
"help": "Help",
"search": "Search",
"assist": "Assist",
"start_conversation": "Start conversation",
"reload_resources": "Reload resources",
"exit_edit_mode": "Done",
"close": "Close"

3106
yarn.lock

File diff suppressed because it is too large Load Diff