20230531.0 (#16697)

This commit is contained in:
Bram Kragten 2023-05-31 15:45:11 +02:00 committed by GitHub
commit 990ade4294
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
369 changed files with 14668 additions and 8208 deletions

View File

@ -14,8 +14,20 @@ not iOS < 13
not dead
[legacy]
# Legacy builds are transpiled to ES5 (strict mode) but also must support some features that cannot be polyfilled:
# Legacy builds are served when modern requirements are not met and support browsers:
# - released in the last 7 years + current alpha/beta versionss
# - with global utilization above 0.05%
# The lattermost query ensures that support for popular old browsers is not dropped too early
# (e.g. IE 11, Android 4.4, or Samsung 4).
#
# In addition, legacy browsers must support some minimum features that cannot be polyfilled:
# - ES5 (strict mode)
# - web sockets to communicate with backend
# - inline SVG used widely in buttons, widgets, etc.
# - custom events used for most user interactions
supports use-strict and supports websockets and supports svg-html5 and supports customevent
# - CSS flexbox used in the majority of the layout
# Nearly all of these are redundant with the above rules.
# As of May 2023, only web sockets must be added to the query.
unreleased versions
last 7 years
> 0.05% and supports websockets

View File

@ -5,9 +5,3 @@ ENV \
DEBIAN_FRONTEND=noninteractive \
DEVCONTAINER=true \
PATH=$PATH:./node_modules/.bin
# Install nvm
COPY .nvmrc /tmp/.nvmrc
RUN \
su vscode -c \
"source /usr/local/share/nvm/nvm.sh && nvm install $(cat /tmp/.nvmrc) 2>&1"

View File

@ -5,7 +5,7 @@
"context": ".."
},
"appPort": "8124:8123",
"postCreateCommand": "script/bootstrap",
"postStartCommand": "script/bootstrap",
"containerEnv": {
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
},

View File

@ -9,7 +9,6 @@ on:
- master
env:
NODE_VERSION: 16
NODE_OPTIONS: --max_old_space_size=6144
jobs:
@ -26,10 +25,10 @@ jobs:
with:
ref: dev
- name: Set up Node ${{ env.NODE_VERSION }}
- name: Setup Node
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
@ -62,10 +61,10 @@ jobs:
with:
ref: master
- name: Set up Node ${{ env.NODE_VERSION }}
- name: Setup Node
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies

View File

@ -11,7 +11,6 @@ on:
- master
env:
NODE_VERSION: 16
NODE_OPTIONS: --max_old_space_size=6144
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -26,10 +25,10 @@ jobs:
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
- name: Set up Node ${{ env.NODE_VERSION }}
- name: Setup Node
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install --immutable
@ -49,10 +48,10 @@ jobs:
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
- name: Set up Node ${{ env.NODE_VERSION }}
- name: Setup Node
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install --immutable
@ -67,10 +66,10 @@ jobs:
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
- name: Set up Node ${{ env.NODE_VERSION }}
- name: Setup Node
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install --immutable
@ -85,10 +84,10 @@ jobs:
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
- name: Set up Node ${{ env.NODE_VERSION }}
- name: Setup Node
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install --immutable

View File

@ -10,7 +10,6 @@ on:
- master
env:
NODE_VERSION: 16
NODE_OPTIONS: --max_old_space_size=6144
jobs:
@ -27,10 +26,10 @@ jobs:
with:
ref: dev
- name: Set up Node ${{ env.NODE_VERSION }}
- name: Setup Node
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
@ -63,10 +62,10 @@ jobs:
with:
ref: master
- name: Set up Node ${{ env.NODE_VERSION }}
- name: Setup Node
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies

View File

@ -6,7 +6,6 @@ on:
- cron: "0 0 * * *"
env:
NODE_VERSION: 16
NODE_OPTIONS: --max_old_space_size=6144
jobs:
@ -19,10 +18,10 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
- name: Set up Node ${{ env.NODE_VERSION }}
- name: Setup Node
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies

View File

@ -11,7 +11,6 @@ on:
- dev
env:
NODE_VERSION: 16
NODE_OPTIONS: --max_old_space_size=6144
jobs:
@ -24,10 +23,10 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
- name: Set up Node ${{ env.NODE_VERSION }}
- name: Setup Node
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies

View File

@ -7,7 +7,6 @@ on:
env:
PYTHON_VERSION: "3.10"
NODE_VERSION: 16
NODE_OPTIONS: --max_old_space_size=6144
permissions:
@ -28,10 +27,10 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Set up Node ${{ env.NODE_VERSION }}
- name: Setup Node
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies

View File

@ -7,7 +7,6 @@ on:
env:
PYTHON_VERSION: "3.10"
NODE_VERSION: 16
NODE_OPTIONS: --max_old_space_size=6144
# Set default workflow permissions
@ -34,10 +33,10 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Set up Node ${{ env.NODE_VERSION }}
- name: Setup Node
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies

View File

@ -7,9 +7,6 @@ on:
paths:
- src/translations/en.json
env:
NODE_VERSION: 16
jobs:
upload:
name: Upload
@ -21,5 +18,4 @@ jobs:
- name: Upload Translations
run: |
export LOKALISE_TOKEN="${{ secrets.LOKALISE_TOKEN }}"
./script/translations_upload_base

2
.nvmrc
View File

@ -1 +1 @@
16
18

View File

@ -0,0 +1,39 @@
diff --git a/modular/sortable.complete.esm.js b/modular/sortable.complete.esm.js
index 02e9f2d6bebeb430fe6e7c1cc3f9c3c9df051f14..bb8268b0844a1faa4108cc92c0be2a3dbaf23f83 100644
--- a/modular/sortable.complete.esm.js
+++ b/modular/sortable.complete.esm.js
@@ -1657,7 +1657,7 @@ Sortable.prototype =
target = parent; // store last element
}
/* jshint boss:true */
- while (parent = parent.parentNode);
+ while (parent = parent.parentNode || parent.getRootNode().host);
}
_unhideGhostForTarget();
diff --git a/modular/sortable.core.esm.js b/modular/sortable.core.esm.js
index b04c8b4634f7c6b4ef1aadbb48afe6564306dea9..39a107163c8c336ebd669b5ea8a936af87e1c1e7 100644
--- a/modular/sortable.core.esm.js
+++ b/modular/sortable.core.esm.js
@@ -1657,7 +1657,7 @@ Sortable.prototype =
target = parent; // store last element
}
/* jshint boss:true */
- while (parent = parent.parentNode);
+ while (parent = parent.parentNode || parent.getRootNode().host);
}
_unhideGhostForTarget();
diff --git a/modular/sortable.esm.js b/modular/sortable.esm.js
index 6ec7ed1bb557e21c2578200161e989c65d23150b..0a05475a22904472fac6c13f524c674da76584b0 100644
--- a/modular/sortable.esm.js
+++ b/modular/sortable.esm.js
@@ -1657,7 +1657,7 @@ Sortable.prototype =
target = parent; // store last element
}
/* jshint boss:true */
- while (parent = parent.parentNode);
+ while (parent = parent.parentNode || parent.getRootNode().host);
}
_unhideGhostForTarget();

File diff suppressed because one or more lines are too long

View File

@ -8,4 +8,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.5.0.cjs
yarnPath: .yarn/releases/yarn-3.5.1.cjs

View File

@ -76,7 +76,7 @@ module.exports.htmlMinifierOptions = {
module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({
safari10: !latestBuild,
ecma: latestBuild ? undefined : 5,
ecma: latestBuild ? 2015 : 5,
format: { comments: false },
sourceMap: !isTestBuild,
});
@ -90,6 +90,8 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
setSpreadProperties: true,
},
browserslistEnv: latestBuild ? "modern" : "legacy",
// Must be unambiguous because some dependencies are CommonJS only
sourceType: "unambiguous",
presets: [
[
"@babel/preset-env",
@ -112,8 +114,6 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
ignoreModuleNotFound: true,
},
],
// Support some proposals still in TC39 process
["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],
// Minify template literals for production
isProdBuild && [
"template-html-minifier",
@ -131,6 +131,13 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
failOnError: true, // we can turn this off in case of false positives
},
],
// Import helpers and regenerator from runtime package
[
"@babel/plugin-transform-runtime",
{ version: require("../package.json").dependencies["@babel/runtime"] },
],
// Support some proposals still in TC39 process
["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],
].filter(Boolean),
exclude: [
// \\ for Windows, / for Mac OS and Linux
@ -149,27 +156,27 @@ const publicPath = (latestBuild, root = "") =>
latestBuild ? `${root}/frontend_latest/` : `${root}/frontend_es5/`;
/*
BundleConfig {
// Object with entrypoints that need to be bundled
entry: { [name: string]: pathToFile },
// Folder where bundled files need to be written
outputPath: string,
// absolute url-path where bundled files can be found
publicPath: string,
// extra definitions that we need to replace in source
defineOverlay: {[name: string]: value },
// if this is a production build
isProdBuild: boolean,
// If we're targeting latest browsers
latestBuild: boolean,
// If we're doing a stats build (create nice chunk names)
isStatsBuild: boolean,
// If it's just a test build in CI, skip time on source map generation
isTestBuild: boolean,
// Names of entrypoints that should not be hashed
dontHash: Set<string>
}
*/
BundleConfig {
// Object with entrypoints that need to be bundled
entry: { [name: string]: pathToFile },
// Folder where bundled files need to be written
outputPath: string,
// absolute url-path where bundled files can be found
publicPath: string,
// extra definitions that we need to replace in source
defineOverlay: {[name: string]: value },
// if this is a production build
isProdBuild: boolean,
// If we're targeting latest browsers
latestBuild: boolean,
// If we're doing a stats build (create nice chunk names)
isStatsBuild: boolean,
// If it's just a test build in CI, skip time on source map generation
isTestBuild: boolean,
// Names of entrypoints that should not be hashed
dontHash: Set<string>
}
*/
module.exports.config = {
app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild, isWDS }) {
@ -252,6 +259,7 @@ module.exports.config = {
isHassioBuild: true,
defineOverlay: {
__SUPERVISOR__: true,
__STATIC_PATH__: `"${paths.hassio_publicPath}/static/"`,
},
};
},

View File

@ -1,18 +1,16 @@
// Run HA develop mode
const gulp = require("gulp");
const env = require("../env.cjs");
require("./clean.cjs");
require("./translations.cjs");
require("./locale-data.cjs");
require("./gen-icons-json.cjs");
require("./gather-static.cjs");
require("./compress.cjs");
require("./webpack.cjs");
require("./service-worker.cjs");
require("./entry-html.cjs");
require("./rollup.cjs");
require("./wds.cjs");
import gulp from "gulp";
import env from "../env.cjs";
import "./clean.js";
import "./compress.js";
import "./entry-html.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./locale-data.js";
import "./rollup.js";
import "./service-worker.js";
import "./translations.js";
import "./wds.js";
import "./webpack.js";
gulp.task(
"develop-app",

View File

@ -1,13 +1,12 @@
const gulp = require("gulp");
const env = require("../env.cjs");
require("./clean.cjs");
require("./translations.cjs");
require("./gather-static.cjs");
require("./webpack.cjs");
require("./service-worker.cjs");
require("./entry-html.cjs");
require("./rollup.cjs");
import gulp from "gulp";
import env from "../env.cjs";
import "./clean.js";
import "./entry-html.js";
import "./gather-static.js";
import "./rollup.js";
import "./service-worker.js";
import "./translations.js";
import "./webpack.js";
gulp.task(
"develop-cast",

View File

@ -1,37 +1,37 @@
const del = import("del");
const gulp = require("gulp");
const paths = require("../paths.cjs");
require("./translations.cjs");
import { deleteSync } from "del";
import gulp from "gulp";
import paths from "../paths.cjs";
import "./translations.js";
gulp.task(
"clean",
gulp.parallel("clean-translations", async () =>
(await del).deleteSync([paths.app_output_root, paths.build_dir])
deleteSync([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])
deleteSync([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])
deleteSync([paths.cast_output_root, paths.build_dir])
)
);
gulp.task("clean-hassio", async () =>
(await del).deleteSync([paths.hassio_output_root, paths.build_dir])
deleteSync([paths.hassio_output_root, paths.build_dir])
);
gulp.task(
"clean-gallery",
gulp.parallel("clean-translations", async () =>
(await del).deleteSync([
deleteSync([
paths.gallery_output_root,
paths.gallery_build,
paths.build_dir,

View File

@ -1,10 +1,10 @@
// Tasks to compress
const gulp = require("gulp");
const zopfli = require("gulp-zopfli-green");
const merge = require("merge-stream");
const path = require("path");
const paths = require("../paths.cjs");
import gulp from "gulp";
import zopfli from "gulp-zopfli-green";
import merge from "merge-stream";
import path from "path";
import paths from "../paths.cjs";
const zopfliOptions = { threshold: 150 };

View File

@ -1,15 +1,13 @@
// Run demo develop mode
const gulp = require("gulp");
const env = require("../env.cjs");
require("./clean.cjs");
require("./translations.cjs");
require("./gen-icons-json.cjs");
require("./gather-static.cjs");
require("./webpack.cjs");
require("./service-worker.cjs");
require("./entry-html.cjs");
require("./rollup.cjs");
import gulp from "gulp";
import env from "../env.cjs";
import "./clean.js";
import "./entry-html.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./rollup.js";
import "./service-worker.js";
import "./translations.js";
import "./webpack.js";
gulp.task(
"develop-demo",

View File

@ -1,6 +1,6 @@
const gulp = require("gulp");
const fs = require("fs/promises");
const mapStream = require("map-stream");
import fs from "fs/promises";
import gulp from "gulp";
import mapStream from "map-stream";
const inDirFrontend = "translations/frontend";
const inDirBackend = "translations/backend";

View File

@ -1,12 +1,13 @@
// Tasks to generate entry HTML
const gulp = require("gulp");
const fs = require("fs-extra");
const path = require("path");
const template = require("lodash.template");
const { minify } = require("html-minifier-terser");
const paths = require("../paths.cjs");
const env = require("../env.cjs");
const { htmlMinifierOptions, terserOptions } = require("../bundle.cjs");
import fs from "fs-extra";
import gulp from "gulp";
import { minify } from "html-minifier-terser";
import template from "lodash.template";
import path from "path";
import { htmlMinifierOptions, terserOptions } from "../bundle.cjs";
import env from "../env.cjs";
import paths from "../paths.cjs";
const renderTemplate = (templateFile, data = {}) => {
const compiled = template(
@ -89,9 +90,11 @@ const genPagesProdTask =
inputSub = "src/html"
) =>
async () => {
const latestManifest = require(path.resolve(outputLatest, "manifest.json"));
const latestManifest = fs.readJsonSync(
path.resolve(outputLatest, "manifest.json")
);
const es5Manifest = outputES5
? require(path.resolve(outputES5, "manifest.json"))
? fs.readJsonSync(path.resolve(outputES5, "manifest.json"))
: {};
const minifiedHTML = [];
for (const [page, entries] of Object.entries(pageEntries)) {

View File

@ -1,15 +1,15 @@
// 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 gulp = require("gulp");
const jszip = require("jszip");
const tar = require("tar");
const { Octokit } = require("@octokit/rest");
const { retry } = require("@octokit/plugin-retry");
const { createOAuthDeviceAuth } = require("@octokit/auth-oauth-device");
import { createOAuthDeviceAuth } from "@octokit/auth-oauth-device";
import { retry } from "@octokit/plugin-retry";
import { Octokit } from "@octokit/rest";
import { deleteAsync } from "del";
import { mkdir, readFile, writeFile } from "fs/promises";
import gulp from "gulp";
import jszip from "jszip";
import path from "path";
import process from "process";
import tar from "tar";
const MAX_AGE = 24; // hours
const OWNER = "home-assistant";
@ -38,7 +38,7 @@ gulp.task("fetch-nightly-translations", async function () {
// and stop if they are not old enough
let currentArtifact;
try {
currentArtifact = JSON.parse(await fs.readFile(ARTIFACT_FILE, "utf-8"));
currentArtifact = JSON.parse(await readFile(ARTIFACT_FILE, "utf-8"));
const currentAge =
(Date.now() - Date.parse(currentArtifact.created_at)) / 3600000;
if (currentAge < MAX_AGE) {
@ -53,7 +53,7 @@ gulp.task("fetch-nightly-translations", async function () {
}
// To store file writing promises
const createExtractDir = fs.mkdir(EXTRACT_DIR, { recursive: true });
const createExtractDir = mkdir(EXTRACT_DIR, { recursive: true });
const writings = [];
// Authenticate to GitHub using GitHub action token if it exists,
@ -63,7 +63,7 @@ gulp.task("fetch-nightly-translations", async function () {
tokenAuth = { token: process.env.GITHUB_TOKEN };
} else {
try {
tokenAuth = JSON.parse(await fs.readFile(TOKEN_FILE, "utf-8"));
tokenAuth = JSON.parse(await readFile(TOKEN_FILE, "utf-8"));
} catch {
if (!allowTokenSetup) {
console.log("No token found so build wil continue with English only");
@ -88,7 +88,7 @@ gulp.task("fetch-nightly-translations", async function () {
tokenAuth = await auth({ type: "oauth" });
writings.push(
createExtractDir.then(
fs.writeFile(TOKEN_FILE, JSON.stringify(tokenAuth, null, 2))
writeFile(TOKEN_FILE, JSON.stringify(tokenAuth, null, 2))
)
);
}
@ -132,17 +132,13 @@ gulp.task("fetch-nightly-translations", async function () {
}
writings.push(
createExtractDir.then(
fs.writeFile(ARTIFACT_FILE, JSON.stringify(latestArtifact, null, 2))
writeFile(ARTIFACT_FILE, JSON.stringify(latestArtifact, null, 2))
)
);
// Remove the current translations
const deleteCurrent = Promise.all(writings).then(
(await del).deleteAsync([
`${EXTRACT_DIR}/*`,
`!${ARTIFACT_FILE}`,
`!${TOKEN_FILE}`,
])
deleteAsync([`${EXTRACT_DIR}/*`, `!${ARTIFACT_FILE}`, `!${TOKEN_FILE}`])
);
// Get the download URL and follow the redirect to download (stored as ArrayBuffer)

View File

@ -1,22 +1,19 @@
// Run demo develop mode
const gulp = require("gulp");
const fs = require("fs");
const path = require("path");
const { marked } = require("marked");
const { glob } = require("glob");
const yaml = require("js-yaml");
const env = require("../env.cjs");
const paths = require("../paths.cjs");
require("./clean.cjs");
require("./translations.cjs");
require("./gen-icons-json.cjs");
require("./gather-static.cjs");
require("./webpack.cjs");
require("./service-worker.cjs");
require("./entry-html.cjs");
require("./rollup.cjs");
import fs from "fs";
import { glob } from "glob";
import gulp from "gulp";
import yaml from "js-yaml";
import { marked } from "marked";
import path from "path";
import env from "../env.cjs";
import paths from "../paths.cjs";
import "./clean.js";
import "./entry-html.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./rollup.js";
import "./service-worker.js";
import "./translations.js";
import "./webpack.js";
gulp.task("gather-gallery-pages", async function gatherPages() {
const pageDir = path.resolve(paths.gallery_dir, "src/pages");

View File

@ -1,9 +1,9 @@
// Gulp task to gather all static files.
const gulp = require("gulp");
const path = require("path");
const fs = require("fs-extra");
const paths = require("../paths.cjs");
import fs from "fs-extra";
import gulp from "gulp";
import path from "path";
import paths from "../paths.cjs";
const npmPath = (...parts) =>
path.resolve(paths.polymer_dir, "node_modules", ...parts);
@ -111,9 +111,10 @@ gulp.task("copy-translations-supervisor", async () => {
copyTranslations(staticDir);
});
gulp.task("copy-locale-data-supervisor", async () => {
gulp.task("copy-static-supervisor", async () => {
const staticDir = paths.hassio_output_static;
copyLocaleData(staticDir);
copyFonts(staticDir);
});
gulp.task("copy-static-app", async () => {

View File

@ -1,17 +1,15 @@
const gulp = require("gulp");
const path = require("path");
const fs = require("fs");
const hash = require("object-hash");
import fs from "fs";
import gulp from "gulp";
import hash from "object-hash";
import path from "path";
import paths from "../paths.cjs";
const ICON_PACKAGE_PATH = path.resolve(
__dirname,
"../../node_modules/@mdi/svg/"
);
const ICON_PACKAGE_PATH = path.resolve("node_modules/@mdi/svg/");
const META_PATH = path.resolve(ICON_PACKAGE_PATH, "meta.json");
const PACKAGE_PATH = path.resolve(ICON_PACKAGE_PATH, "package.json");
const ICON_PATH = path.resolve(ICON_PACKAGE_PATH, "svg");
const OUTPUT_DIR = path.resolve(__dirname, "../../build/mdi");
const REMOVED_ICONS_PATH = path.resolve(__dirname, "../removedIcons.json");
const OUTPUT_DIR = path.resolve(paths.build_dir, "mdi");
const REMOVED_ICONS_PATH = new URL("../removedIcons.json", import.meta.url);
const encoding = "utf8";

View File

@ -1,13 +1,13 @@
const gulp = require("gulp");
const env = require("../env.cjs");
require("./clean.cjs");
require("./compress.cjs");
require("./entry-html.cjs");
require("./gather-static.cjs");
require("./gen-icons-json.cjs");
require("./rollup.cjs");
require("./translations.cjs");
require("./webpack.cjs");
import gulp from "gulp";
import env from "../env.cjs";
import "./clean.js";
import "./compress.js";
import "./entry-html.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./rollup.js";
import "./translations.js";
import "./webpack.js";
gulp.task(
"develop-hassio",
@ -21,7 +21,7 @@ gulp.task(
"build-supervisor-translations",
"copy-translations-supervisor",
"build-locale-data",
"copy-locale-data-supervisor",
"copy-static-supervisor",
env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio"
)
);
@ -37,7 +37,7 @@ gulp.task(
"build-supervisor-translations",
"copy-translations-supervisor",
"build-locale-data",
"copy-locale-data-supervisor",
"copy-static-supervisor",
env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio",
"gen-pages-hassio-prod",
...// Don't compress running tests

View File

@ -1,18 +1,15 @@
const del = import("del");
const path = require("path");
const gulp = require("gulp");
const fs = require("fs");
const paths = require("../paths.cjs");
import { deleteSync } from "del";
import fs from "fs";
import gulp from "gulp";
import path from "path";
import paths from "../paths.cjs";
const outDir = "build/locale-data";
gulp.task("clean-locale-data", async () => (await del).deleteSync([outDir]));
gulp.task("clean-locale-data", async () => deleteSync([outDir]));
gulp.task("ensure-locale-data-build-dir", (done) => {
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir, { recursive: true });
}
done();
gulp.task("ensure-locale-data-build-dir", async () => {
fs.mkdirSync(outDir, { recursive: true });
});
const modules = {
@ -31,11 +28,14 @@ gulp.task("create-locale-data", (done) => {
Object.entries(modules).forEach(([module, className]) => {
Object.keys(translationMeta).forEach((lang) => {
try {
const localeData = String(
fs.readFileSync(
require.resolve(`@formatjs/${module}/locale-data/${lang}.js`)
const localeData = fs
.readFileSync(
path.resolve(
paths.polymer_dir,
`node_modules/@formatjs/${module}/locale-data/${lang}.js`
),
"utf-8"
)
)
.replace(
new RegExp(
`\\/\\*\\s*@generated\\s*\\*\\/\\s*\\/\\/\\s*prettier-ignore\\s*if\\s*\\(Intl\\.${className}\\s*&&\\s*typeof\\s*Intl\\.${className}\\.__addLocaleData\\s*===\\s*'function'\\)\\s*{\\s*Intl\\.${className}\\.__addLocaleData\\(`,
@ -46,15 +46,13 @@ gulp.task("create-locale-data", (done) => {
.replace(/\)\s*}/im, "");
// make sure we have valid JSON
JSON.parse(localeData);
if (!fs.existsSync(path.join(outDir, module))) {
fs.mkdirSync(path.join(outDir, module), { recursive: true });
}
fs.mkdirSync(path.join(outDir, module), { recursive: true });
fs.writeFileSync(
path.join(outDir, `${module}/${lang}.json`),
localeData
);
} catch (e) {
if (e.code !== "MODULE_NOT_FOUND") {
if (e.code !== "ENOENT") {
throw e;
}
}

View File

@ -1,13 +1,14 @@
// Tasks to run Rollup
const path = require("path");
const gulp = require("gulp");
const rollup = require("rollup");
const handler = require("serve-handler");
const http = require("http");
const log = require("fancy-log");
const open = require("open");
const rollupConfig = require("../rollup.cjs");
const paths = require("../paths.cjs");
import log from "fancy-log";
import gulp from "gulp";
import http from "http";
import open from "open";
import path from "path";
import { rollup } from "rollup";
import handler from "serve-handler";
import paths from "../paths.cjs";
import rollupConfig from "../rollup.cjs";
const bothBuilds = (createConfigFunc, params) =>
gulp.series(

View File

@ -1,11 +1,12 @@
// Generate service worker.
// Based on manifest, create a file with the content as service_worker.js
const gulp = require("gulp");
const path = require("path");
const fs = require("fs-extra");
const workboxBuild = require("workbox-build");
const sourceMapUrl = require("source-map-url");
const paths = require("../paths.cjs");
import fs from "fs-extra";
import gulp from "gulp";
import path from "path";
import sourceMapUrl from "source-map-url";
import workboxBuild from "workbox-build";
import paths from "../paths.cjs";
const swDest = path.resolve(paths.app_output_root, "service_worker.js");
@ -28,10 +29,9 @@ self.addEventListener('install', (event) => {
gulp.task("gen-service-worker-app-prod", async () => {
// Read bundled source file
const bundleManifestLatest = require(path.resolve(
paths.app_output_latest,
"manifest.json"
));
const bundleManifestLatest = fs.readJsonSync(
path.resolve(paths.app_output_latest, "manifest.json")
);
let serviceWorkerContent = fs.readFileSync(
paths.app_output_root + bundleManifestLatest["service_worker.js"],
"utf-8"
@ -46,10 +46,9 @@ gulp.task("gen-service-worker-app-prod", async () => {
);
// Remove ES5
const bundleManifestES5 = require(path.resolve(
paths.app_output_es5,
"manifest.json"
));
const bundleManifestES5 = fs.readJsonSync(
path.resolve(paths.app_output_es5, "manifest.json")
);
fs.removeSync(paths.app_output_root + bundleManifestES5["service_worker.js"]);
fs.removeSync(
paths.app_output_root + bundleManifestES5["service_worker.js.map"]

View File

@ -1,19 +1,24 @@
const del = import("del");
const crypto = require("crypto");
const path = require("path");
const source = require("vinyl-source-stream");
const vinylBuffer = require("vinyl-buffer");
const gulp = require("gulp");
const fs = require("fs");
const flatmap = require("gulp-flatmap");
const merge = require("gulp-merge-json");
const rename = require("gulp-rename");
const transform = require("gulp-json-transform");
const { mapFiles } = require("../util.cjs");
const env = require("../env.cjs");
const paths = require("../paths.cjs");
require("./fetch-nightly-translations.cjs");
import { createHash } from "crypto";
import { deleteSync } from "del";
import {
mkdirSync,
readdirSync,
readFileSync,
renameSync,
writeFile,
} from "fs";
import gulp from "gulp";
import flatmap from "gulp-flatmap";
import transform from "gulp-json-transform";
import merge from "gulp-merge-json";
import rename from "gulp-rename";
import path from "path";
import vinylBuffer from "vinyl-buffer";
import source from "vinyl-source-stream";
import env from "../env.cjs";
import paths from "../paths.cjs";
import { mapFiles } from "../util.cjs";
import "./fetch-nightly-translations.js";
const inFrontendDir = "translations/frontend";
const inBackendDir = "translations/backend";
@ -33,7 +38,12 @@ gulp.task(
// Panel translations which should be split from the core translations.
const TRANSLATION_FRAGMENTS = Object.keys(
require("../../src/translations/en.json").ui.panel
JSON.parse(
readFileSync(
path.resolve(paths.polymer_dir, "src/translations/en.json"),
"utf-8"
)
).ui.panel
);
function recursiveFlatten(prefix, data) {
@ -120,17 +130,14 @@ function lokaliseTransform(data, original, file) {
return output;
}
gulp.task("clean-translations", async () => (await del).deleteSync([workDir]));
gulp.task("clean-translations", async () => deleteSync([workDir]));
gulp.task("ensure-translations-build-dir", (done) => {
if (!fs.existsSync(workDir)) {
fs.mkdirSync(workDir, { recursive: true });
}
done();
gulp.task("ensure-translations-build-dir", async () => {
mkdirSync(workDir, { recursive: true });
});
gulp.task("create-test-metadata", (cb) => {
fs.writeFile(
writeFile(
workDir + "/testMetadata.json",
JSON.stringify({
test: {
@ -303,15 +310,14 @@ const fingerprints = {};
gulp.task("build-translation-fingerprints", () => {
// Fingerprint full file of each language
const files = fs.readdirSync(fullDir);
const files = readdirSync(fullDir);
for (let i = 0; i < files.length; i++) {
fingerprints[files[i].split(".")[0]] = {
// In dev we create fake hashes
hash: env.isProdBuild()
? crypto
.createHash("md5")
.update(fs.readFileSync(path.join(fullDir, files[i]), "utf-8"))
? createHash("md5")
.update(readFileSync(path.join(fullDir, files[i]), "utf-8"))
.digest("hex")
: "dev",
};
@ -327,7 +333,7 @@ gulp.task("build-translation-fingerprints", () => {
throw new Error(`Unable to find hash for ${filename}`);
}
fs.renameSync(
renameSync(
filename,
`${parsed.dir}/${parsed.name}-${fingerprints[parsed.name].hash}${
parsed.ext

View File

@ -1,11 +0,0 @@
// Tasks to run Rollup
const gulp = require("gulp");
const { startDevServer } = require("@web/dev-server");
gulp.task("wds-watch-app", () => {
startDevServer({
config: {
watch: true,
},
});
});

10
build-scripts/gulp/wds.js Normal file
View File

@ -0,0 +1,10 @@
import gulp from "gulp";
import { startDevServer } from "@web/dev-server";
gulp.task("wds-watch-app", async () => {
startDevServer({
config: {
watch: true,
},
});
});

View File

@ -1,19 +1,20 @@
// Tasks to run webpack.
const fs = require("fs");
const gulp = require("gulp");
const webpack = require("webpack");
const WebpackDevServer = require("webpack-dev-server");
const log = require("fancy-log");
const path = require("path");
const env = require("../env.cjs");
const paths = require("../paths.cjs");
const {
import log from "fancy-log";
import fs from "fs";
import gulp from "gulp";
import path from "path";
import webpack from "webpack";
import WebpackDevServer from "webpack-dev-server";
import env from "../env.cjs";
import paths from "../paths.cjs";
import {
createAppConfig,
createDemoConfig,
createCastConfig,
createHassioConfig,
createDemoConfig,
createGalleryConfig,
} = require("../webpack.cjs");
createHassioConfig,
} from "../webpack.cjs";
const bothBuilds = (createConfigFunc, params) => [
createConfigFunc({ ...params, latestBuild: true }),

View File

@ -132,6 +132,17 @@ const createWebpackConfig = ({
),
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
// See `src/resources/intl-polyfill-legacy.ts` for explanation
!latestBuild &&
new webpack.NormalModuleReplacementPlugin(
new RegExp(
path.resolve(paths.polymer_dir, "src/resources/intl-polyfill.ts")
),
path.resolve(
paths.polymer_dir,
"src/resources/intl-polyfill-legacy.ts"
)
),
!isProdBuild && new LogStartCompilePlugin(),
].filter(Boolean),
resolve: {

View File

@ -190,7 +190,7 @@ export class HcConnect extends LitElement {
private _handleInputKeyDown(ev: KeyboardEvent) {
// Handle pressing enter.
if (ev.keyCode === 13) {
if (ev.key === "Enter") {
this._handleConnect();
}
}

View File

@ -1,3 +1,5 @@
import { cast } from "chromecast-caf-receiver";
const castContext = cast.framework.CastReceiverContext.getInstance();
const playerManager = castContext.getPlayerManager();

View File

@ -1,2 +1,3 @@
/* eslint-disable no-undef */
import { cast } from "chromecast-caf-receiver";
export const castContext = cast.framework.CastReceiverContext.getInstance();

View File

@ -1,4 +1,4 @@
/* eslint-disable no-undef */
import { cast } from "chromecast-caf-receiver";
import { CAST_NS } from "../../../src/cast/const";
import { HassMessage } from "../../../src/cast/receiver_messages";
import "../../../src/resources/custom-card-support";

View File

@ -84,9 +84,6 @@
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
<%= renderTemplate("../../../src/html/_preload_roboto.html.template") %>
<script>
if (!window.globalThis) {
window.globalThis = window;
}
// Safari 12 and below does not have a compliant ES2015 implementation of template literals, so we ship ES5
if (!isS11_12) {
<% for (const entry of latestEntryJS) { %>

View File

@ -1,7 +1,7 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockConfigEntries = (hass: MockHomeAssistant) => {
hass.mockWS("config_entries/get", () => [
hass.mockWS("config_entries/get_matching", () => [
{
entry_id: "co2signal",
domain: "co2signal",

View File

@ -45,6 +45,10 @@ export default [
header: "Users",
pages: ["user-types", "configuration-menu"],
},
{
category: "date-time",
header: "Date and Time",
},
{
category: "design.home-assistant.io",
header: "About",

View File

@ -1,5 +1,3 @@
import "@polymer/polymer/lib/elements/dom-if";
import "@polymer/polymer/lib/elements/dom-repeat";
import "../../src/resources/ha-style";
import "../../src/resources/roboto";
import "./ha-gallery";

View File

@ -63,7 +63,7 @@ export class DemoAutomationDescribeCondition extends LitElement {
<div class="condition">
<span>
${this._condition
? describeCondition(this._condition, this.hass)
? describeCondition(this._condition, this.hass, [])
: "<invalid YAML>"}
</span>
<ha-yaml-editor
@ -76,7 +76,7 @@ export class DemoAutomationDescribeCondition extends LitElement {
${conditions.map(
(conf) => html`
<div class="condition">
<span>${describeCondition(conf as any, this.hass)}</span>
<span>${describeCondition(conf as any, this.hass, [])}</span>
<pre>${dump(conf)}</pre>
</div>
`

View File

@ -74,7 +74,7 @@ export class DemoAutomationDescribeTrigger extends LitElement {
<div class="trigger">
<span>
${this._trigger
? describeTrigger(this._trigger, this.hass)
? describeTrigger(this._trigger, this.hass, [])
: "<invalid YAML>"}
</span>
<ha-yaml-editor
@ -86,7 +86,7 @@ export class DemoAutomationDescribeTrigger extends LitElement {
${triggers.map(
(conf) => html`
<div class="trigger">
<span>${describeTrigger(conf as any, this.hass)}</span>
<span>${describeTrigger(conf as any, this.hass, [])}</span>
<pre>${dump(conf)}</pre>
</div>
`

View File

@ -0,0 +1,3 @@
---
title: HS Color Picker
---

View File

@ -0,0 +1,120 @@
import "../../../../src/components/ha-hs-color-picker";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-slider";
import { hsv2rgb } from "../../../../src/common/color/convert-color";
@customElement("demo-components-ha-hs-color-picker")
export class DemoHaHsColorPicker extends LitElement {
@state()
brightness = 255;
@state()
value: [number, number] = [0, 0];
@state()
liveValue?: [number, number];
private _brightnessChanged(ev) {
this.brightness = Number(ev.target.value);
}
private _hsColorCursor(ev) {
this.liveValue = ev.detail.value;
}
private _hsColorChanged(ev) {
this.value = ev.detail.value;
}
private _hueChanged(ev) {
this.value = [ev.target.value, this.value[1]];
}
private _saturationChanged(ev) {
this.value = [this.value[0], ev.target.value];
}
protected render(): TemplateResult {
const h = (this.liveValue ?? this.value)[0];
const s = (this.liveValue ?? this.value)[1];
const rgb = hsv2rgb([h, s, this.brightness]);
return html`
<ha-card>
<div class="card-content">
<p class="value">${h}° - ${Math.round(s * 100)}%</p>
<p class="value">${rgb.map((v) => Math.round(v)).join(", ")}</p>
<ha-hs-color-picker
colorBrightness=${this.brightness}
.value=${this.value}
@value-changed=${this._hsColorChanged}
@cursor-moved=${this._hsColorCursor}
></ha-hs-color-picker>
<p>Hue : ${this.value[0]}</p>
<ha-slider
step="1"
pin
min="0"
max="360"
.value=${this.value[0]}
@change=${this._hueChanged}
>
</ha-slider>
<p>Saturation : ${this.value[1]}</p>
<ha-slider
step="0.01"
pin
min="0"
max="1"
.value=${this.value[1]}
@change=${this._saturationChanged}
>
</ha-slider>
<p>Color Brighness : ${this.brightness}</p>
<ha-slider
step="1"
pin
min="0"
max="255"
.value=${this.brightness}
@change=${this._brightnessChanged}
>
</ha-slider>
</div>
</ha-card>
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
.card-content {
display: flex;
align-items: center;
flex-direction: column;
}
ha-hs-color-picker {
width: 400px;
}
.value {
font-size: 22px;
font-weight: bold;
margin: 0 0 12px 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-hs-color-picker": DemoHaHsColorPicker;
}
}

View File

@ -0,0 +1,3 @@
---
title: Temp Color Picker
---

View File

@ -0,0 +1,117 @@
import "../../../../src/components/ha-temp-color-picker";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-slider";
@customElement("demo-components-ha-temp-color-picker")
export class DemoHaTempColorPicker extends LitElement {
@state()
min = 3000;
@state()
max = 7000;
@state()
value = 4000;
@state()
liveValue?: number;
private _minChanged(ev) {
this.min = Number(ev.target.value);
}
private _maxChanged(ev) {
this.max = Number(ev.target.value);
}
private _valueChanged(ev) {
this.value = Number(ev.target.value);
}
private _tempColorCursor(ev) {
this.liveValue = ev.detail.value;
}
private _tempColorChanged(ev) {
this.value = ev.detail.value;
}
protected render(): TemplateResult {
return html`
<ha-card>
<div class="card-content">
<p class="value">${this.liveValue ?? this.value} K</p>
<ha-temp-color-picker
.min=${this.min}
.max=${this.max}
.value=${this.value}
@value-changed=${this._tempColorChanged}
@cursor-moved=${this._tempColorCursor}
></ha-temp-color-picker>
<p>Min temp : ${this.min} K</p>
<ha-slider
step="1"
pin
min="2000"
max="10000"
.value=${this.min}
@change=${this._minChanged}
>
</ha-slider>
<p>Max temp : ${this.max} K</p>
<ha-slider
step="1"
pin
min="2000"
max="10000"
.value=${this.max}
@change=${this._maxChanged}
>
</ha-slider>
<p>Value : ${this.value} K</p>
<ha-slider
step="1"
pin
min=${this.min}
max=${this.max}
.value=${this.value}
@change=${this._valueChanged}
>
</ha-slider>
</div>
</ha-card>
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
.card-content {
display: flex;
align-items: center;
flex-direction: column;
}
ha-temp-color-picker {
width: 400px;
}
.value {
font-size: 22px;
font-weight: bold;
margin: 0 0 12px 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-temp-color-picker": DemoHaTempColorPicker;
}
}

View File

@ -0,0 +1,7 @@
---
title: (Numeric) Date Formatting
---
This pages lists all supported languages with their available (numeric) date formats.
Formatting function: `const formatDateNumeric: (dateObj: Date, locale: FrontendLocaleData) => string`

View File

@ -0,0 +1,106 @@
import { html, css, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../src/components/ha-card";
import { HomeAssistant } from "../../../../src/types";
import { translationMetadata } from "../../../../src/resources/translations-metadata";
import { formatDateNumeric } from "../../../../src/common/datetime/format_date";
import {
FrontendLocaleData,
NumberFormat,
TimeFormat,
DateFormat,
FirstWeekday,
} from "../../../../src/data/translation";
@customElement("demo-date-time-date")
export class DemoDateTimeDate extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
protected render() {
const defaultLocale: FrontendLocaleData = {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.language,
date_format: DateFormat.language,
first_weekday: FirstWeekday.language,
};
const date = new Date();
return html`
<mwc-list>
<div class="container header">
<div>Language</div>
<div class="center">Default (lang)</div>
<div class="center">Day-Month-Year</div>
<div class="center">Month-Day-Year</div>
<div class="center">Year-Month-Day</div>
</div>
${Object.entries(translationMetadata.translations)
.filter(([key, _]) => key !== "test")
.map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatDateNumeric(date, {
...defaultLocale,
language: key,
date_format: DateFormat.language,
})}
</div>
<div class="center">
${formatDateNumeric(date, {
...defaultLocale,
language: key,
date_format: DateFormat.DMY,
})}
</div>
<div class="center">
${formatDateNumeric(date, {
...defaultLocale,
language: key,
date_format: DateFormat.MDY,
})}
</div>
<div class="center">
${formatDateNumeric(date, {
...defaultLocale,
language: key,
date_format: DateFormat.YMD,
})}
</div>
</div>
`
)}
</mwc-list>
`;
}
static get styles() {
return css`
.header {
font-weight: bold;
}
.center {
text-align: center;
}
.container {
max-width: 600px;
margin: 12px auto;
display: flex;
align-items: center;
justify-content: space-evenly;
}
.container > div {
flex-grow: 1;
width: 20%;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-date-time-date": DemoDateTimeDate;
}
}

View File

@ -50,6 +50,7 @@ const SENSOR_DEVICE_CLASSES = [
"temperature",
"timestamp",
"volatile_organic_compounds",
"volatile_organic_compounds_parts",
"voltage",
"volume",
"water",

View File

@ -130,6 +130,7 @@ export class HassioAddonRepositoryEl extends LitElement {
css`
ha-card {
cursor: pointer;
overflow: hidden;
}
.not_available {
opacity: 0.6;

View File

@ -70,7 +70,7 @@ export class HassioAddonStore extends LitElement {
text: extractApiErrorMessage(err),
});
} finally {
await this._loadData();
this._loadData();
}
}
@ -99,10 +99,10 @@ export class HassioAddonStore extends LitElement {
slot="trigger"
></ha-icon-button>
<mwc-list-item>
${this.supervisor.localize("store.repositories")}
${this.supervisor.localize("store.check_updates")}
</mwc-list-item>
<mwc-list-item>
${this.supervisor.localize("store.check_updates")}
${this.supervisor.localize("store.repositories")}
</mwc-list-item>
${this.hass.userData?.showAdvanced &&
atLeastVersion(this.hass.config.version, 0, 117)
@ -177,10 +177,10 @@ export class HassioAddonStore extends LitElement {
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._manageRepositoriesClicked();
this.refreshData();
break;
case 1:
this.refreshData();
this._manageRepositoriesClicked();
break;
case 2:
this._manageRegistries();
@ -198,18 +198,18 @@ export class HassioAddonStore extends LitElement {
this._manageRepositories();
}
private async _manageRepositories(url?: string) {
private _manageRepositories(url?: string) {
showRepositoriesDialog(this, {
supervisor: this.supervisor,
url,
});
}
private async _manageRegistries() {
private _manageRegistries() {
showRegistriesDialog(this, { supervisor: this.supervisor });
}
private async _loadData() {
private _loadData() {
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",

View File

@ -140,7 +140,12 @@ class HassioAddonConfig extends LitElement {
? {
name: entry.name,
required: entry.required,
selector: { number: { mode: "box" } },
selector: {
number: {
mode: "box",
step: entry.type === "float" ? "any" : undefined,
},
},
}
: entry
)

View File

@ -3,11 +3,11 @@ import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiBackupRestore, mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
@ -26,9 +26,9 @@ import "../../../src/components/ha-fab";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-svg-icon";
import {
HassioBackup,
fetchHassioBackups,
friendlyFolderName,
HassioBackup,
reloadHassioBackups,
removeBackup,
} from "../../../src/data/hassio/backup";
@ -43,6 +43,7 @@ import type { HaTabsSubpageDataTable } from "../../../src/layouts/hass-tabs-subp
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types";
import { showBackupUploadDialog } from "../dialogs/backup/show-dialog-backup-upload";
import { showHassioBackupLocationDialog } from "../dialogs/backup/show-dialog-hassio-backu-location";
import { showHassioBackupDialog } from "../dialogs/backup/show-dialog-hassio-backup";
import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hassio-create-backup";
import { supervisorTabs } from "../hassio-tabs";
@ -204,6 +205,9 @@ export class HassioBackups extends LitElement {
<mwc-list-item>
${this.supervisor.localize("common.reload")}
</mwc-list-item>
<mwc-list-item>
${this.supervisor.localize("dialog.backup_location.title")}
</mwc-list-item>
${atLeastVersion(this.hass.config.version, 0, 116)
? html`<mwc-list-item>
${this.supervisor.localize("backup.upload_backup")}
@ -270,6 +274,9 @@ export class HassioBackups extends LitElement {
this.refreshData();
break;
case 1:
showHassioBackupLocationDialog(this, { supervisor: this.supervisor });
break;
case 2:
this._showUploadBackupDialog();
break;
}

View File

@ -5,8 +5,8 @@ import {
CSSResultGroup,
html,
LitElement,
TemplateResult,
nothing,
TemplateResult,
} from "lit";
import { customElement, property, query } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
@ -23,8 +23,11 @@ import {
HassioPartialBackupCreateParams,
} from "../../../src/data/hassio/backup";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { PolymerChangedEvent } from "../../../src/polymer-types";
import { HomeAssistant, TranslationDict } from "../../../src/types";
import {
HomeAssistant,
TranslationDict,
ValueChangedEvent,
} from "../../../src/types";
import "./supervisor-formfield-label";
type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
@ -416,7 +419,7 @@ export class SupervisorBackupContent extends LitElement {
this[input.name] = input.value;
}
private _handleTextValueChanged(ev: PolymerChangedEvent<string>) {
private _handleTextValueChanged(ev: ValueChangedEvent<string>) {
const input = ev.currentTarget as PaperInputElement;
this[input.name!] = ev.detail.value;
}

View File

@ -1,10 +1,13 @@
import { mdiArrowUpBoldCircle, mdiPuzzle } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { navigate } from "../../../src/common/navigate";
import { caseInsensitiveStringCompare } from "../../../src/common/string/compare";
import "../../../src/components/ha-card";
import "../../../src/components/search-input";
import { HassioAddonInfo } from "../../../src/data/hassio/addon";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
@ -17,11 +20,25 @@ class HassioAddons extends LitElement {
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow!: boolean;
@state() private _filter?: string;
protected render(): TemplateResult {
return html`
<div class="search">
<search-input
.hass=${this.hass}
suffix
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
.label=${this.supervisor.localize("dashboard.search_addons")}
>
</search-input>
</div>
<div class="content">
${!atLeastVersion(this.hass.config.version, 2021, 12)
? html` <h1>${this.supervisor.localize("dashboard.addons")}</h1> `
? html`<h1>${this.supervisor.localize("dashboard.addons")}</h1>`
: ""}
<div class="card-group">
${!this.supervisor.addon.addons.length
@ -34,67 +51,73 @@ class HassioAddons extends LitElement {
</div>
</ha-card>
`
: this.supervisor.addon.addons
.sort((a, b) =>
caseInsensitiveStringCompare(
a.name,
b.name,
this.hass.locale.language
)
)
.map(
(addon) => html`
<ha-card
outlined
.addon=${addon}
@click=${this._addonTapped}
>
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title=${addon.name}
.description=${addon.description}
available
.showTopbar=${addon.update_available}
topbarClass="update"
.icon=${addon.update_available!
? mdiArrowUpBoldCircle
: mdiPuzzle}
.iconTitle=${addon.state !== "started"
? this.supervisor.localize(
"dashboard.addon_stopped"
)
: addon.update_available!
? this.supervisor.localize(
"dashboard.addon_new_version"
)
: this.supervisor.localize(
"dashboard.addon_running"
)}
.iconClass=${addon.update_available
? addon.state === "started"
? "update"
: "update stopped"
: addon.state === "started"
? "running"
: "stopped"}
.iconImage=${atLeastVersion(
this.hass.config.version,
0,
105
) && addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined}
></hassio-card-content>
</div>
</ha-card>
`
)}
: this._getAddons(this.supervisor.addon.addons, this._filter).map(
(addon) => html`
<ha-card outlined .addon=${addon} @click=${this._addonTapped}>
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title=${addon.name}
.description=${addon.description}
available
.showTopbar=${addon.update_available}
topbarClass="update"
.icon=${addon.update_available!
? mdiArrowUpBoldCircle
: mdiPuzzle}
.iconTitle=${addon.state !== "started"
? this.supervisor.localize("dashboard.addon_stopped")
: addon.update_available!
? this.supervisor.localize(
"dashboard.addon_new_version"
)
: this.supervisor.localize("dashboard.addon_running")}
.iconClass=${addon.update_available
? addon.state === "started"
? "update"
: "update stopped"
: addon.state === "started"
? "running"
: "stopped"}
.iconImage=${atLeastVersion(
this.hass.config.version,
0,
105
) && addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined}
></hassio-card-content>
</div>
</ha-card>
`
)}
</div>
</div>
`;
}
private _getAddons = memoizeOne(
(addons: HassioAddonInfo[], filter?: string) => {
if (filter) {
addons = addons.filter((addon) => {
const lowerCaseFilter = filter.toLowerCase();
return (
addon.name.toLowerCase().includes(lowerCaseFilter) ||
addon.description.toLowerCase().includes(lowerCaseFilter) ||
addon.slug.toLowerCase().includes(lowerCaseFilter)
);
});
}
return addons.sort((a, b) =>
caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language)
);
}
);
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,
@ -102,6 +125,17 @@ class HassioAddons extends LitElement {
css`
ha-card {
cursor: pointer;
overflow: hidden;
}
.search {
position: sticky;
top: 0;
z-index: 2;
}
search-input {
display: block;
--mdc-text-field-fill-color: var(--sidebar-background-color);
--mdc-text-field-idle-line-color: var(--divider-color);
}
`,
];

View File

@ -1,4 +1,4 @@
import { mdiStorePlus } from "@mdi/js";
import { mdiStorePlus, mdiUpdate } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
@ -10,6 +10,10 @@ import { HomeAssistant, Route } from "../../../src/types";
import { supervisorTabs } from "../hassio-tabs";
import "./hassio-addons";
import "../../../src/layouts/hass-subpage";
import { reloadHassioAddons } from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import { fireEvent } from "../../../src/common/dom/fire_event";
@customElement("hassio-dashboard")
class HassioDashboard extends LitElement {
@ -36,9 +40,16 @@ class HassioDashboard extends LitElement {
back-path="/config"
.header=${this.supervisor.localize("panel.addons")}
>
<ha-icon-button
slot="toolbar-icon"
@click=${this._handleCheckUpdates}
.path=${mdiUpdate}
.label=${this.supervisor.localize("store.check_updates")}
></ha-icon-button>
<hassio-addons
.hass=${this.hass}
.supervisor=${this.supervisor}
.narrow=${this.narrow}
></hassio-addons>
<a href="/hassio/store">
<ha-fab
@ -99,6 +110,18 @@ class HassioDashboard extends LitElement {
`;
}
private async _handleCheckUpdates() {
try {
await reloadHassioAddons(this.hass);
} catch (err) {
showAlertDialog(this, {
text: extractApiErrorMessage(err),
});
} finally {
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
}
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@ -0,0 +1,154 @@
import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../src/components/ha-form/types";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { changeMountOptions } from "../../../../src/data/supervisor/mounts";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { HassioBackupLocationDialogParams } from "./show-dialog-hassio-backu-location";
const SCHEMA = memoizeOne(
() =>
[
{
name: "default_backup_mount",
required: true,
selector: { backup_location: {} },
},
] as const
);
@customElement("dialog-hassio-backup-location")
class HassioBackupLocationDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false })
public _dialogParams?: HassioBackupLocationDialogParams;
@state() private _data?: { default_backup_mount: string | null };
@state() private _waiting?: boolean;
@state() private _error?: string;
public async showDialog(
dialogParams: HassioBackupLocationDialogParams
): Promise<void> {
this._dialogParams = dialogParams;
}
public closeDialog(): void {
this._data = undefined;
this._error = undefined;
this._waiting = undefined;
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._dialogParams) {
return nothing;
}
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
.heading=${this._dialogParams.supervisor.localize(
"dialog.backup_location.title"
)}
@closed=${this.closeDialog}
>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<ha-form
.hass=${this.hass}
.data=${this._data}
.schema=${SCHEMA()}
.computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
@value-changed=${this._valueChanged}
dialogInitialFocus
></ha-form>
<mwc-button
slot="secondaryAction"
@click=${this.closeDialog}
dialogInitialFocus
>
${this._dialogParams.supervisor.localize("common.cancel")}
</mwc-button>
<mwc-button
.disabled=${this._waiting || !this._data}
slot="primaryAction"
@click=${this._changeMount}
>
${this._dialogParams.supervisor.localize("common.save")}
</mwc-button>
</ha-dialog>
`;
}
private _computeLabelCallback = (
// @ts-ignore
schema: SchemaUnion<ReturnType<typeof SCHEMA>>
): string =>
this._dialogParams!.supervisor.localize(
`dialog.backup_location.options.${schema.name}.name`
) || schema.name;
private _computeHelperCallback = (
// @ts-ignore
schema: SchemaUnion<ReturnType<typeof SCHEMA>>
): string =>
this._dialogParams!.supervisor.localize(
`dialog.backup_location.options.${schema.name}.description`
);
private _valueChanged(ev: CustomEvent) {
const newLocation = ev.detail.value.default_backup_mount;
this._data = {
default_backup_mount: newLocation === "/backup" ? null : newLocation,
};
}
private async _changeMount() {
if (!this._data) {
return;
}
this._error = undefined;
this._waiting = true;
try {
await changeMountOptions(this.hass, this._data);
} catch (err: any) {
this._error = extractApiErrorMessage(err);
this._waiting = false;
return;
}
this.closeDialog();
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
.delete-btn {
--mdc-theme-primary: var(--error-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-backup-location": HassioBackupLocationDialog;
}
}

View File

@ -0,0 +1,17 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioBackupLocationDialogParams {
supervisor: Supervisor;
}
export const showHassioBackupLocationDialog = (
element: HTMLElement,
dialogParams: HassioBackupLocationDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-backup-location",
dialogImport: () => import("./dialog-hassio-backup-location"),
dialogParams,
});
};

View File

@ -218,7 +218,7 @@ class HassioRepositoriesDialog extends LitElement {
private _handleKeyAdd(ev: KeyboardEvent) {
ev.stopPropagation();
if (ev.keyCode !== 13) {
if (ev.key !== "Enter") {
return;
}
this._addRepository();

View File

@ -1,4 +1,5 @@
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
import { Supervisor } from "../../src/data/supervisor/supervisor";
import {
@ -22,9 +23,15 @@ class HassioRouter extends HassRouterPage {
protected routerOptions: RouterOptions = {
// Hass.io has a page with tabs, so we route all non-matching routes to it.
defaultPage: "dashboard",
beforeRender: (page: string) =>
page === "snapshots" ? "backups" : undefined,
initialLoad: () => this._redirectIngress(),
beforeRender: (page: string) => {
if (page === "snapshots") {
return "backups";
}
if (page === "dashboard" && this.panel.config?.ingress) {
return "ingress";
}
return undefined;
},
showLoading: true,
routes: {
dashboard: {
@ -55,32 +62,28 @@ class HassioRouter extends HassRouterPage {
protected updatePageEl(el) {
// the tabs page does its own routing so needs full route.
const hassioPanel = el.nodeName === "HASSIO-PANEL";
const route = hassioPanel ? this.route : this.routeTail;
if (hassioPanel && this.panel.config?.ingress) {
this._redirectIngress();
return;
}
const hassioPanel = el.localName === "hassio-panel";
const ingressPanel = el.localName === "hassio-ingress-view";
const route = hassioPanel
? this.route
: ingressPanel && this.panel.config?.ingress
? this._ingressRoute(this.panel.config?.ingress)
: this.routeTail;
el.hass = this.hass;
el.narrow = this.narrow;
el.route = route;
el.supervisor = this.supervisor;
if (el.localName === "hassio-ingress-view") {
el.ingressPanel = this.panel.config && this.panel.config.ingress;
if (ingressPanel) {
el.ingressPanel = Boolean(this.panel.config?.ingress);
}
}
private async _redirectIngress() {
if (this.panel.config && this.panel.config.ingress) {
this.route = {
prefix: "/hassio",
path: `/ingress/${this.panel.config.ingress}`,
};
}
}
private _ingressRoute = memoizeOne((ingress: string) => ({
prefix: "/hassio/ingress",
path: `/${ingress}`,
}));
}
declare global {

View File

@ -53,78 +53,54 @@ export class SupervisorBaseElement extends urlSyncMixin(
public connectedCallback(): void {
super.connectedCallback();
this._initializeLocalize();
if (!this.hasUpdated) {
return;
}
if (this.route?.prefix === "/hassio") {
this._initSupervisor();
}
}
public disconnectedCallback() {
super.disconnectedCallback();
Object.keys(this._unsubs).forEach((unsub) => {
this._unsubs[unsub]();
delete this._unsubs[unsub];
});
this.removeEventListener(
"supervisor-collection-refresh",
this._handleSupervisorStoreRefreshEvent
);
}
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
protected willUpdate(changedProperties: PropertyValues) {
if (!this.hasUpdated) {
if (this.route?.prefix === "/hassio") {
this._initSupervisor();
}
}
if (changedProperties.has("hass")) {
const oldHass = changedProperties.get("hass") as
| HomeAssistant
| undefined;
if (
oldHass !== undefined &&
oldHass.language !== undefined &&
oldHass.language !== this.hass.language
) {
if (oldHass?.language !== this.hass.language) {
this._language = this.hass.language;
}
}
if (changedProperties.has("_language")) {
if (changedProperties.get("_language") !== this._language) {
this._initializeLocalize();
}
}
if (changedProperties.has("_collections")) {
if (this._collections) {
const unsubs = Object.keys(this._unsubs);
for (const collection of Object.keys(this._collections)) {
if (!unsubs.includes(collection)) {
this._unsubs[collection] = this._collections[collection].subscribe(
(data) => this._updateSupervisor({ [collection]: data })
);
}
}
}
if (changedProperties.has("_language") || !this.hasUpdated) {
this._initializeLocalize();
}
}
protected _updateSupervisor(obj: Partial<Supervisor>): void {
this.supervisor = { ...this.supervisor, ...obj };
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
if (
this._language !== this.hass.language &&
this.hass.language !== undefined
) {
this._language = this.hass.language;
}
this._initializeLocalize();
if (this.route?.prefix === "/hassio") {
this._initSupervisor();
}
protected _updateSupervisor(update: Partial<Supervisor>): void {
this.supervisor = { ...this.supervisor, ...update };
}
private async _initializeLocalize() {
const { language, data } = await getTranslation(
null,
this._language,
"/api/hassio/app/static/translations"
);
const { language, data } = await getTranslation(null, this._language);
this.supervisor = {
...this.supervisor,
this._updateSupervisor({
localize: await computeLocalize<SupervisorKeys>(
this.constructor.prototype,
language,
@ -132,7 +108,7 @@ export class SupervisorBaseElement extends urlSyncMixin(
[language]: data,
}
),
};
});
}
private async _handleSupervisorStoreRefreshEvent(ev) {
@ -149,6 +125,17 @@ export class SupervisorBaseElement extends urlSyncMixin(
this._updateSupervisor({ [collection]: response.data });
}
private _subscribeCollection(collection: string) {
if (this._unsubs[collection]) {
this._unsubs[collection]();
}
this._unsubs[collection] = this._collections[collection].subscribe((data) =>
this._updateSupervisor({
[collection]: data,
})
);
}
private async _initSupervisor(): Promise<void> {
this.addEventListener(
"supervisor-collection-refresh",
@ -158,6 +145,7 @@ export class SupervisorBaseElement extends urlSyncMixin(
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
Object.keys(supervisorCollection).forEach((collection) => {
if (collection in this._collections) {
this._subscribeCollection(collection);
this._collections[collection].refresh();
} else {
this._collections[collection] = getSupervisorEventCollection(
@ -165,17 +153,13 @@ export class SupervisorBaseElement extends urlSyncMixin(
collection,
supervisorCollection[collection]
);
}
});
Object.keys(this._collections).forEach((collection) => {
if (
this.supervisor === undefined ||
this.supervisor[collection] === undefined
) {
this._updateSupervisor({
[collection]: this._collections[collection].state,
});
if (this._collections[collection].state) {
// happens when the grace period of the collection unsubscribe has not passed yet
this._updateSupervisor({
[collection]: this._collections[collection].state,
});
}
this._subscribeCollection(collection);
}
});
} else {

View File

@ -25,32 +25,33 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.22.3",
"@braintree/sanitize-url": "6.0.2",
"@codemirror/autocomplete": "6.5.1",
"@codemirror/commands": "6.2.3",
"@codemirror/language": "6.6.0",
"@codemirror/autocomplete": "6.7.1",
"@codemirror/commands": "6.2.4",
"@codemirror/language": "6.7.0",
"@codemirror/legacy-modes": "6.3.2",
"@codemirror/search": "6.3.0",
"@codemirror/state": "6.2.0",
"@codemirror/view": "6.9.5",
"@codemirror/search": "6.4.0",
"@codemirror/state": "6.2.1",
"@codemirror/view": "6.12.0",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.7.0",
"@formatjs/intl-displaynames": "6.3.1",
"@formatjs/intl-getcanonicallocales": "2.1.0",
"@formatjs/intl-locale": "3.2.1",
"@formatjs/intl-numberformat": "8.4.1",
"@formatjs/intl-pluralrules": "5.2.1",
"@formatjs/intl-relativetimeformat": "11.2.1",
"@fullcalendar/core": "6.1.5",
"@fullcalendar/daygrid": "6.1.5",
"@fullcalendar/interaction": "6.1.5",
"@fullcalendar/list": "6.1.5",
"@fullcalendar/timegrid": "6.1.5",
"@lezer/highlight": "1.1.4",
"@lit-labs/context": "0.3.0",
"@formatjs/intl-datetimeformat": "6.8.0",
"@formatjs/intl-displaynames": "6.3.2",
"@formatjs/intl-getcanonicallocales": "2.2.0",
"@formatjs/intl-locale": "3.3.0",
"@formatjs/intl-numberformat": "8.5.0",
"@formatjs/intl-pluralrules": "5.2.2",
"@formatjs/intl-relativetimeformat": "11.2.2",
"@fullcalendar/core": "6.1.8",
"@fullcalendar/daygrid": "6.1.8",
"@fullcalendar/interaction": "6.1.8",
"@fullcalendar/list": "6.1.8",
"@fullcalendar/timegrid": "6.1.8",
"@lezer/highlight": "1.1.6",
"@lit-labs/context": "0.3.1",
"@lit-labs/motion": "1.0.3",
"@lit-labs/virtualizer": "1.0.1",
"@lrnwebcomponents/simple-tooltip": "4.1.0",
"@lit-labs/virtualizer": "2.0.2",
"@lrnwebcomponents/simple-tooltip": "7.0.0",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-button": "0.27.0",
@ -76,7 +77,7 @@
"@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "=1.0.0-pre.4",
"@material/web": "=1.0.0-pre.9",
"@mdi/js": "7.2.96",
"@mdi/svg": "7.2.96",
"@polymer/app-layout": "3.1.0",
@ -91,8 +92,8 @@
"@polymer/paper-toast": "3.0.1",
"@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "23.3.11",
"@vaadin/vaadin-themable-mixin": "23.3.11",
"@vaadin/combo-box": "24.0.7",
"@vaadin/vaadin-themable-mixin": "24.0.7",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@ -102,30 +103,29 @@
"app-datepicker": "5.1.1",
"chart.js": "3.3.2",
"comlink": "4.4.1",
"core-js": "3.30.1",
"core-js": "3.30.2",
"cropperjs": "1.5.13",
"date-fns": "2.29.3",
"date-fns": "2.30.0",
"date-fns-tz": "2.0.0",
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"fuse.js": "6.6.2",
"google-timezones-json": "1.1.0",
"hls.js": "1.3.5",
"hls.js": "1.4.4",
"home-assistant-js-websocket": "8.0.1",
"idb-keyval": "6.2.0",
"intl-messageformat": "10.3.4",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.3.5",
"js-yaml": "4.1.0",
"leaflet": "1.9.3",
"leaflet": "1.9.4",
"leaflet-draw": "1.0.4",
"lit": "2.7.2",
"lit": "2.7.4",
"marked": "4.3.0",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2",
"punycode": "2.3.0",
"qr-scanner": "1.4.2",
"qrcode": "1.5.1",
"regenerator-runtime": "0.13.11",
"qrcode": "1.5.3",
"resize-observer-polyfill": "1.5.1",
"roboto-fontface": "0.10.0",
"rrule": "2.7.2",
@ -140,30 +140,32 @@
"vue": "2.7.14",
"vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.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",
"workbox-cacheable-response": "6.6.0",
"workbox-core": "6.6.0",
"workbox-expiration": "6.6.0",
"workbox-precaching": "6.6.0",
"workbox-routing": "6.6.0",
"workbox-strategies": "6.6.0",
"xss": "1.0.14"
},
"devDependencies": {
"@babel/core": "7.21.4",
"@babel/plugin-proposal-decorators": "7.21.0",
"@babel/preset-env": "7.21.4",
"@babel/preset-typescript": "7.21.4",
"@babel/core": "7.22.1",
"@babel/plugin-proposal-decorators": "7.22.3",
"@babel/plugin-transform-runtime": "7.22.2",
"@babel/preset-env": "7.22.2",
"@babel/preset-typescript": "7.21.5",
"@koa/cors": "4.0.0",
"@octokit/auth-oauth-device": "4.0.4",
"@octokit/plugin-retry": "4.1.3",
"@octokit/rest": "19.0.7",
"@octokit/rest": "19.0.11",
"@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.3",
"@rollup/plugin-commonjs": "24.1.0",
"@rollup/plugin-commonjs": "25.0.0",
"@rollup/plugin-json": "6.0.0",
"@rollup/plugin-node-resolve": "15.0.2",
"@rollup/plugin-replace": "5.0.2",
"@types/chromecast-caf-receiver": "5.0.12",
"@types/babel__plugin-transform-runtime": "7.9.2",
"@types/chromecast-caf-receiver": "6.0.9",
"@types/chromecast-caf-sender": "1.0.5",
"@types/esprima": "4.0.3",
"@types/glob": "8.1.0",
@ -171,22 +173,22 @@
"@types/js-yaml": "4.0.5",
"@types/leaflet": "1.9.3",
"@types/leaflet-draw": "1.0.6",
"@types/marked": "4.0.8",
"@types/marked": "4.3.1",
"@types/mocha": "10.0.1",
"@types/qrcode": "1.5.0",
"@types/serve-handler": "6.1.1",
"@types/sortablejs": "1.15.1",
"@types/tar": "6.1.4",
"@types/tar": "6.1.5",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "5.59.0",
"@typescript-eslint/parser": "5.59.0",
"@typescript-eslint/eslint-plugin": "5.59.7",
"@typescript-eslint/parser": "5.59.7",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.2",
"babel-plugin-template-html-minifier": "4.1.0",
"chai": "4.3.7",
"del": "7.0.0",
"eslint": "8.38.0",
"eslint": "8.41.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.0.0",
"eslint-config-prettier": "8.8.0",
@ -196,11 +198,11 @@
"eslint-plugin-lit": "1.8.3",
"eslint-plugin-lit-a11y": "2.4.1",
"eslint-plugin-unused-imports": "2.0.0",
"eslint-plugin-wc": "1.4.0",
"eslint-plugin-wc": "1.5.0",
"esprima": "4.0.1",
"fancy-log": "2.0.0",
"fs-extra": "11.1.1",
"glob": "10.2.1",
"glob": "10.2.6",
"gulp": "4.0.2",
"gulp-flatmap": "1.0.2",
"gulp-json-transform": "0.4.8",
@ -211,7 +213,7 @@
"husky": "8.0.3",
"instant-mocha": "1.5.1",
"jszip": "3.10.1",
"lint-staged": "13.2.1",
"lint-staged": "13.2.2",
"lit-analyzer": "1.2.1",
"lodash.template": "4.5.0",
"magic-string": "0.30.0",
@ -219,38 +221,39 @@
"merge-stream": "2.0.0",
"mocha": "10.2.0",
"object-hash": "3.0.0",
"open": "8.4.2",
"open": "9.1.0",
"pinst": "3.0.0",
"prettier": "2.8.7",
"prettier": "2.8.8",
"rollup": "2.79.1",
"rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.9.0",
"serve-handler": "6.1.5",
"sinon": "15.0.4",
"sinon": "15.1.0",
"source-map-url": "0.4.1",
"systemjs": "6.14.1",
"tar": "6.1.13",
"terser-webpack-plugin": "5.3.7",
"tar": "6.1.15",
"terser-webpack-plugin": "5.3.9",
"ts-lit-plugin": "1.2.1",
"typescript": "4.9.5",
"vinyl-buffer": "1.0.1",
"vinyl-source-stream": "2.0.0",
"webpack": "=5.72.1",
"webpack-cli": "5.0.1",
"webpack-dev-server": "4.13.3",
"webpack": "5.84.1",
"webpack-cli": "5.1.1",
"webpack-dev-server": "4.15.0",
"webpack-manifest-plugin": "5.0.0",
"webpackbar": "5.0.2",
"workbox-build": "6.5.4"
"workbox-build": "6.6.0"
},
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
"resolutions": {
"@polymer/polymer": "patch:@polymer/polymer@3.5.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@material/mwc-button@^0.25.3": "^0.27.0"
"@material/mwc-button@^0.25.3": "^0.27.0",
"sortablejs@1.15.0": "patch:sortablejs@npm%3A1.15.0#./.yarn/patches/sortablejs-npm-1.15.0-f3a393abcc.patch"
},
"prettier": {
"trailingComma": "es5",
"arrowParens": "always"
},
"packageManager": "yarn@3.5.0"
"packageManager": "yarn@3.5.1"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

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

View File

@ -1,4 +1,4 @@
#!/bin/sh
#!/bin/bash -i
# Resolve all frontend dependencies that the application requires to develop.
# Stop on errors
@ -6,5 +6,17 @@ set -e
cd "$(dirname "$0")/.."
# Install/upgrade node when inside devcontainer
if [[ -n "$DEVCONTAINER" ]]; then
nodeCurrent=$(nvm version default || echo "")
nodeLatest=$(nvm version-remote "$(cat .nvmrc)")
if [[ -z "$nodeCurrent" ]]; then
nvm install
elif [[ "$nodeCurrent" != "$nodeLatest" ]]; then
nvm install --reinstall-packages-from="$nodeCurrent" --default
nvm uninstall "$nodeCurrent"
fi
fi
# Install node modules
yarn install
yarn install

View File

@ -105,7 +105,7 @@ export class HaAuthFlow extends LitElement {
}
this.addEventListener("keypress", (ev) => {
if (ev.keyCode === 13) {
if (ev.key === "Enter") {
this._handleSubmit(ev);
}
});

View File

@ -1,8 +1,5 @@
/* eslint-disable no-undef, no-console */
import {
CastStateEventData,
SessionStateEventData,
} from "chromecast-caf-receiver/cast.framework";
/* eslint-disable no-console */
import { Auth } from "home-assistant-js-websocket";
import { castApiAvailable } from "./cast_framework";
import { CAST_APP_ID, CAST_DEV, CAST_NS } from "./const";
@ -48,11 +45,11 @@ export class CastManager {
});
context.addEventListener(
cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
(ev) => this._sessionStateChanged(ev)
this._sessionStateChanged
);
context.addEventListener(
cast.framework.CastContextEventType.CAST_STATE_CHANGED,
(ev) => this._castStateChanged(ev)
this._castStateChanged
);
}
@ -119,7 +116,7 @@ export class CastManager {
}
}
private _sessionStateChanged(ev: SessionStateEventData) {
private _sessionStateChanged = (ev: cast.framework.SessionStateEventData) => {
if (__DEV__) {
console.log("Cast session state changed", ev.sessionState);
}
@ -140,14 +137,14 @@ export class CastManager {
this.status = undefined;
this._fireEvent("connection-changed");
}
}
};
private _castStateChanged(ev: CastStateEventData) {
private _castStateChanged = (ev: cast.framework.CastStateEventData) => {
if (__DEV__) {
console.log("Cast state changed", ev.castState);
}
this._fireEvent("state-changed");
}
};
private _attachMessageListener() {
const session = this.castSession;

View File

@ -0,0 +1,106 @@
import { clamp } from "../number/clamp";
const DEFAULT_MIN_KELVIN = 2700;
const DEFAULT_MAX_KELVIN = 6500;
export const temperature2rgb = (
temperature: number
): [number, number, number] => {
const value = temperature / 100;
return [
temperatureRed(value),
temperatureGreen(value),
temperatureBlue(value),
];
};
const temperatureRed = (temperature: number): number => {
if (temperature <= 66) {
return 255;
}
const red = 329.698727446 * (temperature - 60) ** -0.1332047592;
return clamp(red, 0, 255);
};
const temperatureGreen = (temperature: number): number => {
let green: number;
if (temperature <= 66) {
green = 99.4708025861 * Math.log(temperature) - 161.1195681661;
} else {
green = 288.1221695283 * (temperature - 60) ** -0.0755148492;
}
return clamp(green, 0, 255);
};
const temperatureBlue = (temperature: number): number => {
if (temperature >= 66) {
return 255;
}
if (temperature <= 19) {
return 0;
}
const blue = 138.5177312231 * Math.log(temperature - 10) - 305.0447927307;
return clamp(blue, 0, 255);
};
const matchMaxScale = (
inputColors: number[],
outputColors: number[]
): number[] => {
const maxIn: number = Math.max(...inputColors);
const maxOut: number = Math.max(...outputColors);
let factor: number;
if (maxOut === 0) {
factor = 0.0;
} else {
factor = maxIn / maxOut;
}
return outputColors.map((value) => Math.round(value * factor));
};
const mired2kelvin = (miredTemperature: number) =>
Math.floor(1000000 / miredTemperature);
const kelvin2mired = (kelvintTemperature: number) =>
Math.floor(1000000 / kelvintTemperature);
export const rgbww2rgb = (
rgbww: [number, number, number, number, number],
minKelvin?: number,
maxKelvin?: number
): [number, number, number] => {
const [r, g, b, cw, ww] = rgbww;
// Calculate color temperature of the white channels
const maxMireds: number = kelvin2mired(minKelvin ?? DEFAULT_MIN_KELVIN);
const minMireds: number = kelvin2mired(maxKelvin ?? DEFAULT_MAX_KELVIN);
const miredRange: number = maxMireds - minMireds;
let ctRatio: number;
try {
ctRatio = ww / (cw + ww);
} catch (_error) {
ctRatio = 0.5;
}
const colorTempMired = minMireds + ctRatio * miredRange;
const colorTempKelvin = colorTempMired ? mired2kelvin(colorTempMired) : 0;
const [wR, wG, wB] = temperature2rgb(colorTempKelvin);
const whiteLevel = Math.max(cw, ww) / 255;
// Add the white channels to the rgb channels.
const rgb = [
r + wR * whiteLevel,
g + wG * whiteLevel,
b + wB * whiteLevel,
] as [number, number, number];
// Match the output maximum value to the input. This ensures the
// output doesn't overflow.
return matchMaxScale([r, g, b, cw, ww], rgb) as [number, number, number];
};
export const rgbw2rgb = (
rgbw: [number, number, number, number]
): [number, number, number] => {
const [r, g, b, w] = rgbw;
const rgb = [r + w, g + w, b + w] as [number, number, number];
return matchMaxScale([r, g, b, w], rgb) as [number, number, number];
};

View File

@ -83,6 +83,7 @@ export const FIXED_DOMAIN_ICONS = {
configurator: mdiCog,
conversation: mdiMicrophoneMessage,
counter: mdiCounter,
date: mdiCalendar,
demo: mdiHomeAssistant,
google_assistant: mdiGoogleAssistant,
group: mdiGoogleCirclesCommunities,
@ -159,6 +160,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
temperature: mdiThermometer,
timestamp: mdiClock,
volatile_organic_compounds: mdiMolecule,
volatile_organic_compounds_parts: mdiMolecule,
voltage: mdiSineWave,
volume: mdiCarCoolantLevel,
water: mdiWater,
@ -208,6 +210,8 @@ export const DOMAINS_INPUT_ROW = [
"automation",
"button",
"cover",
"date",
"datetime",
"fan",
"group",
"humidifier",
@ -226,6 +230,7 @@ export const DOMAINS_INPUT_ROW = [
"select",
"switch",
"text",
"time",
"vacuum",
];

View File

@ -0,0 +1,23 @@
import { isSameDay, isSameYear } from "date-fns";
import { FrontendLocaleData } from "../../data/translation";
import {
formatShortDateTime,
formatShortDateTimeWithYear,
} from "./format_date_time";
import { formatTime } from "./format_time";
export const absoluteTime = (
from: Date,
locale: FrontendLocaleData,
to?: Date
): string => {
const _to = to ?? new Date();
if (isSameDay(from, _to)) {
return formatTime(from, locale);
}
if (isSameYear(from, _to)) {
return formatShortDateTime(from, locale);
}
return formatShortDateTimeWithYear(from, locale);
};

View File

@ -1,11 +1,7 @@
import { getWeekStartByLocale } from "weekstart";
import { FrontendLocaleData, FirstWeekday } from "../../data/translation";
import { polyfillsLoaded } from "../translations/localize";
if (__BUILD__ === "latest" && polyfillsLoaded) {
await polyfillsLoaded;
}
import "../../resources/intl-polyfill";
export const weekdays = [
"sunday",

View File

@ -1,10 +1,6 @@
import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation";
import { polyfillsLoaded } from "../translations/localize";
if (__BUILD__ === "latest" && polyfillsLoaded) {
await polyfillsLoaded;
}
import { FrontendLocaleData, DateFormat } from "../../data/translation";
import "../../resources/intl-polyfill";
// Tuesday, August 10
export const formatDateWeekdayDay = (
@ -35,17 +31,63 @@ const formatDateMem = memoizeOne(
);
// 10/08/2021
export const formatDateNumeric = (dateObj: Date, locale: FrontendLocaleData) =>
formatDateNumericMem(locale).format(dateObj);
export const formatDateNumeric = (
dateObj: Date,
locale: FrontendLocaleData
) => {
const formatter = formatDateNumericMem(locale);
const formatDateNumericMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
if (
locale.date_format === DateFormat.language ||
locale.date_format === DateFormat.system
) {
return formatter.format(dateObj);
}
const parts = formatter.formatToParts(dateObj);
const literal = parts.find((value) => value.type === "literal")?.value;
const day = parts.find((value) => value.type === "day")?.value;
const month = parts.find((value) => value.type === "month")?.value;
const year = parts.find((value) => value.type === "year")?.value;
const lastPart = parts.at(parts.length - 1);
let lastLiteral = lastPart?.type === "literal" ? lastPart?.value : "";
if (locale.language === "bg" && locale.date_format === DateFormat.YMD) {
lastLiteral = "";
}
const formats = {
[DateFormat.DMY]: `${day}${literal}${month}${literal}${year}${lastLiteral}`,
[DateFormat.MDY]: `${month}${literal}${day}${literal}${year}${lastLiteral}`,
[DateFormat.YMD]: `${year}${literal}${month}${literal}${day}${lastLiteral}`,
};
return formats[locale.date_format];
};
const formatDateNumericMem = memoizeOne((locale: FrontendLocaleData) => {
const localeString =
locale.date_format === DateFormat.system ? undefined : locale.language;
if (
locale.date_format === DateFormat.language ||
locale.date_format === DateFormat.system
) {
return new Intl.DateTimeFormat(localeString, {
year: "numeric",
month: "numeric",
day: "numeric",
})
);
});
}
return new Intl.DateTimeFormat(localeString, {
year: "numeric",
month: "numeric",
day: "numeric",
});
});
// Aug 10
export const formatDateShort = (dateObj: Date, locale: FrontendLocaleData) =>

View File

@ -1,11 +1,9 @@
import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation";
import { polyfillsLoaded } from "../translations/localize";
import "../../resources/intl-polyfill";
import { useAmPm } from "./use_am_pm";
if (__BUILD__ === "latest" && polyfillsLoaded) {
await polyfillsLoaded;
}
import { formatDateNumeric } from "./format_date";
import { formatTime } from "./format_time";
// August 9, 2021, 8:23 AM
export const formatDateTime = (dateObj: Date, locale: FrontendLocaleData) =>
@ -28,6 +26,29 @@ const formatDateTimeMem = memoizeOne(
)
);
// Aug 9, 2021, 8:23 AM
export const formatShortDateTimeWithYear = (
dateObj: Date,
locale: FrontendLocaleData
) => formatShortDateTimeWithYearMem(locale).format(dateObj);
const formatShortDateTimeWithYearMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
year: "numeric",
month: "short",
day: "numeric",
hour: useAmPm(locale) ? "numeric" : "2-digit",
minute: "2-digit",
hour12: useAmPm(locale),
}
)
);
// Aug 9, 8:23 AM
export const formatShortDateTime = (
dateObj: Date,
@ -78,21 +99,4 @@ const formatDateTimeWithSecondsMem = memoizeOne(
export const formatDateTimeNumeric = (
dateObj: Date,
locale: FrontendLocaleData
) => formatDateTimeNumericMem(locale).format(dateObj);
const formatDateTimeNumericMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: useAmPm(locale),
}
)
);
) => `${formatDateNumeric(dateObj, locale)}, ${formatTime(dateObj, locale)}`;

View File

@ -1,12 +1,8 @@
import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation";
import { polyfillsLoaded } from "../translations/localize";
import "../../resources/intl-polyfill";
import { useAmPm } from "./use_am_pm";
if (__BUILD__ === "latest" && polyfillsLoaded) {
await polyfillsLoaded;
}
// 9:15 PM || 21:15
export const formatTime = (dateObj: Date, locale: FrontendLocaleData) =>
formatTimeMem(locale).format(dateObj);

View File

@ -1,12 +1,8 @@
import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation";
import { polyfillsLoaded } from "../translations/localize";
import "../../resources/intl-polyfill";
import { selectUnit } from "../util/select-unit";
if (__BUILD__ === "latest" && polyfillsLoaded) {
await polyfillsLoaded;
}
const formatRelTimeMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.RelativeTimeFormat(locale.language, { numeric: "auto" })

View File

@ -5,7 +5,8 @@ import type { ClassElement } from "../../types";
type Callback = (oldValue: any, newValue: any) => void;
class Storage {
constructor(subscribe = true) {
constructor(subscribe = true, storage = window.localStorage) {
this.storage = storage;
if (!subscribe) {
return;
}
@ -26,6 +27,8 @@ class Storage {
});
}
public storage: globalThis.Storage;
private _storage: { [storageKey: string]: any } = {};
private _listeners: {
@ -34,7 +37,7 @@ class Storage {
public addFromStorage(storageKey: any): void {
if (!this._storage[storageKey]) {
const data = window.localStorage.getItem(storageKey);
const data = this.storage.getItem(storageKey);
if (data) {
this._storage[storageKey] = JSON.parse(data);
}
@ -77,9 +80,9 @@ class Storage {
this._storage[storageKey] = value;
try {
if (value === undefined) {
window.localStorage.removeItem(storageKey);
this.storage.removeItem(storageKey);
} else {
window.localStorage.setItem(storageKey, JSON.stringify(value));
this.storage.setItem(storageKey, JSON.stringify(value));
}
} catch (err: any) {
// Safari in private mode doesn't allow localstorage
@ -94,10 +97,14 @@ export const LocalStorage =
storageKey?: string,
property?: boolean,
subscribe = true,
storageType?: globalThis.Storage,
propertyOptions?: PropertyDeclaration
): any =>
(clsElement: ClassElement) => {
const storage = subscribe ? subscribeStorage : new Storage(false);
const storage =
subscribe && !storageType
? subscribeStorage
: new Storage(subscribe, storageType);
const key = String(clsElement.key);
storageKey = storageKey || String(clsElement.key);

View File

@ -1,5 +1,10 @@
import type { LitElement } from "lit";
import type { ClassElement } from "../../types";
import { throttle } from "../util/throttle";
const throttleReplaceState = throttle((value) => {
history.replaceState({ scrollPosition: value }, "");
}, 300);
export const restoreScroll =
(selector: string): any =>
@ -9,10 +14,13 @@ export const restoreScroll =
key: element.key,
descriptor: {
set(this: LitElement, value: number) {
throttleReplaceState(value);
this[`__${String(element.key)}`] = value;
},
get(this: LitElement) {
return this[`__${String(element.key)}`];
return (
this[`__${String(element.key)}`] || history.state?.scrollPosition
);
},
enumerable: true,
configurable: true,
@ -21,12 +29,17 @@ export const restoreScroll =
const connectedCallback = cls.prototype.connectedCallback;
cls.prototype.connectedCallback = function () {
connectedCallback.call(this);
if (this[element.key]) {
const target = this.renderRoot.querySelector(selector);
if (!target) {
return;
}
target.scrollTop = this[element.key];
const scrollPos = this[element.key];
if (scrollPos) {
this.updateComplete.then(() => {
const target = this.renderRoot.querySelector(selector);
if (!target) {
return;
}
setTimeout(() => {
target.scrollTop = scrollPos;
}, 0);
});
}
};
},

View File

@ -1,4 +1,4 @@
export const isNavigationClick = (e: MouseEvent) => {
export const isNavigationClick = (e: MouseEvent, preventDefault = true) => {
// Taken from polymer/pwa-helpers. BSD-3 licensed
if (
e.defaultPrevented ||
@ -40,6 +40,8 @@ export const isNavigationClick = (e: MouseEvent) => {
return undefined;
}
e.preventDefault();
if (preventDefault) {
e.preventDefault();
}
return href;
};

View File

@ -45,7 +45,7 @@ export const computeAttributeValueDisplay = (
// If invalid URL, exception will be raised
const url = new URL(attributeValue);
if (url.protocol === "http:" || url.protocol === "https:")
return html`<a target="_blank" rel="noreferrer" href=${value}
return html`<a target="_blank" rel="noreferrer" href=${attributeValue}
>${attributeValue}</a
>`;
} catch (_) {

View File

@ -117,59 +117,39 @@ export const computeStateDisplayFromEntityAttributes = (
const domain = computeDomain(entityId);
if (domain === "input_datetime") {
if (state !== undefined) {
// If trying to display an explicit state, need to parse the explicit state to `Date` then format.
// Attributes aren't available, we have to use `state`.
try {
const components = state.split(" ");
if (components.length === 2) {
// Date and time.
return formatDateTime(new Date(components.join("T")), locale);
if (domain === "datetime") {
const time = new Date(state);
return formatDateTime(time, locale);
}
if (["date", "input_datetime", "time"].includes(domain)) {
// If trying to display an explicit state, need to parse the explicit state to `Date` then format.
// Attributes aren't available, we have to use `state`.
try {
const components = state.split(" ");
if (components.length === 2) {
// Date and time.
return formatDateTime(new Date(components.join("T")), locale);
}
if (components.length === 1) {
if (state.includes("-")) {
// Date only.
return formatDate(new Date(`${state}T00:00`), locale);
}
if (components.length === 1) {
if (state.includes("-")) {
// Date only.
return formatDate(new Date(`${state}T00:00`), locale);
}
if (state.includes(":")) {
// Time only.
const now = new Date();
return formatTime(
new Date(`${now.toISOString().split("T")[0]}T${state}`),
locale
);
}
if (state.includes(":")) {
// Time only.
const now = new Date();
return formatTime(
new Date(`${now.toISOString().split("T")[0]}T${state}`),
locale
);
}
return state;
} catch (_e) {
// Formatting methods may throw error if date parsing doesn't go well,
// just return the state string in that case.
return state;
}
} else {
// If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format.
let date: Date;
if (attributes.has_date && attributes.has_time) {
date = new Date(
attributes.year,
attributes.month - 1,
attributes.day,
attributes.hour,
attributes.minute
);
return formatDateTime(date, locale);
}
if (attributes.has_date) {
date = new Date(attributes.year, attributes.month - 1, attributes.day);
return formatDate(date, locale);
}
if (attributes.has_time) {
date = new Date();
date.setHours(attributes.hour, attributes.minute);
return formatTime(date, locale);
}
return state;
} catch (_e) {
// Formatting methods may throw error if date parsing doesn't go well,
// just return the state string in that case.
return state;
}
}

View File

@ -188,6 +188,7 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
"temperature",
"timestamp",
"volatile_organic_compounds",
"volatile_organic_compounds_parts",
"voltage",
],
state_class: ["measurement", "total", "total_increasing"],

View File

@ -1,11 +1,6 @@
import { shouldPolyfill as shouldPolyfillLocale } from "@formatjs/intl-locale/lib/should-polyfill";
import { shouldPolyfill as shouldPolyfillPluralRules } from "@formatjs/intl-pluralrules/lib/should-polyfill";
import { shouldPolyfill as shouldPolyfillRelativeTime } from "@formatjs/intl-relativetimeformat/lib/should-polyfill";
import { shouldPolyfill as shouldPolyfillDateTime } from "@formatjs/intl-datetimeformat/lib/should-polyfill";
import { shouldPolyfill as shouldPolyfillDisplayName } from "@formatjs/intl-displaynames/lib/should-polyfill";
import IntlMessageFormat from "intl-messageformat";
import { polyfillLocaleData } from "../../resources/locale-data-polyfill";
import { Resources, TranslationDict } from "../../types";
import { getLocalLanguage } from "../../util/common-translation";
// Exclude some patterns from key type checking for now
// These are intended to be removed as errors are fixed
@ -64,40 +59,6 @@ export interface FormatsType {
time: FormatType;
}
const loadedPolyfillLocale = new Set();
const locale = getLocalLanguage();
const polyfills: Promise<any>[] = [];
if (__BUILD__ === "latest") {
if (shouldPolyfillLocale()) {
await import("@formatjs/intl-locale/polyfill");
}
if (shouldPolyfillPluralRules(locale)) {
polyfills.push(import("@formatjs/intl-pluralrules/polyfill"));
polyfills.push(import("@formatjs/intl-pluralrules/locale-data/en"));
}
if (shouldPolyfillRelativeTime(locale)) {
polyfills.push(import("@formatjs/intl-relativetimeformat/polyfill"));
}
if (shouldPolyfillDateTime(locale)) {
polyfills.push(import("@formatjs/intl-datetimeformat/polyfill"));
polyfills.push(import("@formatjs/intl-datetimeformat/add-all-tz"));
}
if (shouldPolyfillDisplayName(locale)) {
polyfills.push(import("@formatjs/intl-displaynames/polyfill"));
polyfills.push(import("@formatjs/intl-displaynames/locale-data/en"));
}
}
export const polyfillsLoaded =
polyfills.length === 0
? undefined
: Promise.all(polyfills).then(() =>
// Load the default language
loadPolyfillLocales(locale)
);
/**
* Adapted from Polymer app-localize-behavior.
*
@ -125,11 +86,9 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>(
resources: Resources,
formats?: FormatsType
): Promise<LocalizeFunc<Keys>> => {
if (polyfillsLoaded) {
await polyfillsLoaded;
}
await loadPolyfillLocales(language);
await import("../../resources/intl-polyfill").then(() =>
polyfillLocaleData(language)
);
// Every time any of the parameters change, invalidate the strings cache.
cache._localizationCache = {};
@ -181,58 +140,3 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>(
}
};
};
export const loadPolyfillLocales = async (language: string) => {
if (loadedPolyfillLocale.has(language)) {
return;
}
loadedPolyfillLocale.add(language);
try {
if (
Intl.NumberFormat &&
// @ts-ignore
typeof Intl.NumberFormat.__addLocaleData === "function"
) {
const result = await fetch(
`/static/locale-data/intl-numberformat/${language}.json`
);
// @ts-ignore
Intl.NumberFormat.__addLocaleData(await result.json());
}
if (
Intl.RelativeTimeFormat &&
// @ts-ignore
typeof Intl.RelativeTimeFormat.__addLocaleData === "function"
) {
const result = await fetch(
`/static/locale-data/intl-relativetimeformat/${language}.json`
);
// @ts-ignore
Intl.RelativeTimeFormat.__addLocaleData(await result.json());
}
if (
Intl.DateTimeFormat &&
// @ts-ignore
typeof Intl.DateTimeFormat.__addLocaleData === "function"
) {
const result = await fetch(
`/static/locale-data/intl-datetimeformat/${language}.json`
);
// @ts-ignore
Intl.DateTimeFormat.__addLocaleData(await result.json());
}
if (
Intl.DisplayNames &&
// @ts-ignore
typeof Intl.DisplayNames.__addLocaleData === "function"
) {
const result = await fetch(
`/static/locale-data/intl-displaynames/${language}.json`
);
// @ts-ignore
Intl.DisplayNames.__addLocaleData(await result.json());
}
} catch (e) {
// Ignore
}
};

View File

@ -63,7 +63,7 @@ class StateHistoryChartLine extends LitElement {
animation: false,
interaction: {
mode: "nearest",
axis: "x",
axis: "xy",
},
scales: {
x: {

View File

@ -2,6 +2,7 @@ import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import millisecondsToDuration from "../../common/datetime/milliseconds_to_duration";
import { fireEvent } from "../../common/dom/fire_event";
import { numberFormatToLocale } from "../../common/number/format_number";
import { computeRTL } from "../../common/util/compute_rtl";
@ -173,10 +174,16 @@ export class StateHistoryChartTimeline extends LitElement {
beforeBody: (context) => context[0].dataset.label || "",
label: (item) => {
const d = item.dataset.data[item.dataIndex] as TimeLineData;
const durationInMs = d.end.getTime() - d.start.getTime();
const formattedDuration = `${this.hass.localize(
"ui.components.history_charts.duration"
)}: ${millisecondsToDuration(durationInMs)}`;
return [
d.label || "",
formatDateTimeWithSeconds(d.start, this.hass.locale),
formatDateTimeWithSeconds(d.end, this.hass.locale),
formattedDuration,
];
},
labelColor: (item) => ({

View File

@ -1,4 +1,3 @@
import "@lit-labs/virtualizer";
import {
css,
CSSResultGroup,
@ -15,6 +14,7 @@ import {
LineChartUnit,
TimelineEntity,
} from "../../data/history";
import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
import "./state-history-chart-line";
import "./state-history-chart-timeline";
@ -171,6 +171,12 @@ export class StateHistoryCharts extends LitElement {
return !(changedProps.size === 1 && changedProps.has("hass"));
}
protected willUpdate() {
if (!this.hasUpdated) {
loadVirtualizer();
}
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("_chartCount")) {
if (this._chartCount < this._childYWidths.length) {

View File

@ -32,12 +32,7 @@ import {
import type { HomeAssistant } from "../../types";
import "./ha-chart-base";
export type ExtendedStatisticType = StatisticType | "change";
export const supportedStatTypeMap: Record<
ExtendedStatisticType,
StatisticType
> = {
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
mean: "mean",
min: "min",
max: "max",
@ -46,15 +41,6 @@ export const supportedStatTypeMap: Record<
change: "sum",
};
export const statTypeMap: Record<ExtendedStatisticType, StatisticType> = {
mean: "mean",
min: "min",
max: "max",
sum: "sum",
state: "state",
change: "sum",
};
@customElement("statistics-chart")
class StatisticsChart extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -72,7 +58,7 @@ class StatisticsChart extends LitElement {
@property({ attribute: false }) public endTime?: Date;
@property({ type: Array }) public statTypes: Array<ExtendedStatisticType> = [
@property({ type: Array }) public statTypes: Array<StatisticType> = [
"sum",
"min",
"mean",
@ -358,7 +344,7 @@ class StatisticsChart extends LitElement {
: this.statTypes;
sortedTypes.forEach((type) => {
if (statisticsHaveType(stats, statTypeMap[type])) {
if (statisticsHaveType(stats, type)) {
const band = drawBands && (type === "min" || type === "max");
statTypes.push(type);
statDataSets.push({
@ -391,7 +377,6 @@ class StatisticsChart extends LitElement {
let prevDate: Date | null = null;
// Process chart data.
let firstSum: number | null | undefined = null;
let prevSum: number | null | undefined = null;
stats.forEach((stat) => {
const startDate = new Date(stat.start);
if (prevDate === startDate) {
@ -408,13 +393,6 @@ class StatisticsChart extends LitElement {
} else {
val = (stat.sum || 0) - firstSum;
}
} else if (type === "change") {
if (prevSum === null || prevSum === undefined) {
prevSum = stat.sum;
return;
}
val = (stat.sum || 0) - prevSum;
prevSum = stat.sum;
} else {
val = stat[type];
}

View File

@ -1,286 +0,0 @@
import memoizeOne from "memoize-one";
import { caseInsensitiveStringCompare } from "../common/string/compare";
export const COUNTRIES = [
"AD",
"AE",
"AF",
"AG",
"AI",
"AL",
"AM",
"AO",
"AQ",
"AR",
"AS",
"AT",
"AU",
"AW",
"AX",
"AZ",
"BA",
"BB",
"BD",
"BE",
"BF",
"BG",
"BH",
"BI",
"BJ",
"BL",
"BM",
"BN",
"BO",
"BQ",
"BR",
"BS",
"BT",
"BV",
"BW",
"BY",
"BZ",
"CA",
"CC",
"CD",
"CF",
"CG",
"CH",
"CI",
"CK",
"CL",
"CM",
"CN",
"CO",
"CR",
"CU",
"CV",
"CW",
"CX",
"CY",
"CZ",
"DE",
"DJ",
"DK",
"DM",
"DO",
"DZ",
"EC",
"EE",
"EG",
"EH",
"ER",
"ES",
"ET",
"FI",
"FJ",
"FK",
"FM",
"FO",
"FR",
"GA",
"GB",
"GD",
"GE",
"GF",
"GG",
"GH",
"GI",
"GL",
"GM",
"GN",
"GP",
"GQ",
"GR",
"GS",
"GT",
"GU",
"GW",
"GY",
"HK",
"HM",
"HN",
"HR",
"HT",
"HU",
"ID",
"IE",
"IL",
"IM",
"IN",
"IO",
"IQ",
"IR",
"IS",
"IT",
"JE",
"JM",
"JO",
"JP",
"KE",
"KG",
"KH",
"KI",
"KM",
"KN",
"KP",
"KR",
"KW",
"KY",
"KZ",
"LA",
"LB",
"LC",
"LI",
"LK",
"LR",
"LS",
"LT",
"LU",
"LV",
"LY",
"MA",
"MC",
"MD",
"ME",
"MF",
"MG",
"MH",
"MK",
"ML",
"MM",
"MN",
"MO",
"MP",
"MQ",
"MR",
"MS",
"MT",
"MU",
"MV",
"MW",
"MX",
"MY",
"MZ",
"NA",
"NC",
"NE",
"NF",
"NG",
"NI",
"NL",
"NO",
"NP",
"NR",
"NU",
"NZ",
"OM",
"PA",
"PE",
"PF",
"PG",
"PH",
"PK",
"PL",
"PM",
"PN",
"PR",
"PS",
"PT",
"PW",
"PY",
"QA",
"RE",
"RO",
"RS",
"RU",
"RW",
"SA",
"SB",
"SC",
"SD",
"SE",
"SG",
"SH",
"SI",
"SJ",
"SK",
"SL",
"SM",
"SN",
"SO",
"SR",
"SS",
"ST",
"SV",
"SX",
"SY",
"SZ",
"TC",
"TD",
"TF",
"TG",
"TH",
"TJ",
"TK",
"TL",
"TM",
"TN",
"TO",
"TR",
"TT",
"TV",
"TW",
"TZ",
"UA",
"UG",
"UM",
"US",
"UY",
"UZ",
"VA",
"VC",
"VE",
"VG",
"VI",
"VN",
"VU",
"WF",
"WS",
"YE",
"YT",
"ZA",
"ZM",
"ZW",
];
export const getCountryOptions = memoizeOne((language?: string) => {
const countryDisplayNames =
Intl && "DisplayNames" in Intl
? new Intl.DisplayNames(language, {
type: "region",
fallback: "code",
})
: undefined;
const options = COUNTRIES.map((country) => ({
value: country,
label: countryDisplayNames ? countryDisplayNames.of(country)! : country,
}));
options.sort((a, b) =>
caseInsensitiveStringCompare(a.label, b.label, language)
);
return options;
});
export const createCountryListEl = (language?: string) => {
const list = document.createElement("datalist");
list.id = "countries";
const options = getCountryOptions(language);
for (const country of options) {
const option = document.createElement("option");
option.value = country.value;
option.innerText = country.label;
list.appendChild(option);
}
return list;
};

View File

@ -1,192 +0,0 @@
import memoizeOne from "memoize-one";
import { caseInsensitiveStringCompare } from "../common/string/compare";
export const CURRENCIES = [
"AED",
"AFN",
"ALL",
"AMD",
"ANG",
"AOA",
"ARS",
"AUD",
"AWG",
"AZN",
"BAM",
"BBD",
"BDT",
"BGN",
"BHD",
"BIF",
"BMD",
"BND",
"BOB",
"BRL",
"BSD",
"BTN",
"BWP",
"BYN",
"BYR",
"BZD",
"CAD",
"CDF",
"CHF",
"CLP",
"CNY",
"COP",
"CRC",
"CUP",
"CVE",
"CZK",
"DJF",
"DKK",
"DOP",
"DZD",
"EGP",
"ERN",
"ETB",
"EUR",
"FJD",
"FKP",
"GBP",
"GEL",
"GHS",
"GIP",
"GMD",
"GNF",
"GTQ",
"GYD",
"HKD",
"HNL",
"HRK",
"HTG",
"HUF",
"IDR",
"ILS",
"INR",
"IQD",
"IRR",
"ISK",
"JMD",
"JOD",
"JPY",
"KES",
"KGS",
"KHR",
"KMF",
"KPW",
"KRW",
"KWD",
"KYD",
"KZT",
"LAK",
"LBP",
"LKR",
"LRD",
"LSL",
"LTL",
"LYD",
"MAD",
"MDL",
"MGA",
"MKD",
"MMK",
"MNT",
"MOP",
"MRO",
"MUR",
"MVR",
"MWK",
"MXN",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR",
"NZD",
"OMR",
"PAB",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"RWF",
"SAR",
"SBD",
"SCR",
"SDG",
"SEK",
"SGD",
"SHP",
"SLL",
"SOS",
"SRD",
"SSP",
"STD",
"SYP",
"SZL",
"THB",
"TJS",
"TMT",
"TND",
"TOP",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"USD",
"UYU",
"UZS",
"VEF",
"VND",
"VUV",
"WST",
"XAF",
"XCD",
"XOF",
"XPF",
"YER",
"ZAR",
"ZMW",
"ZWL",
];
export const getCurrencyOptions = memoizeOne((language?: string) => {
const currencyDisplayNames =
Intl && "DisplayNames" in Intl
? new Intl.DisplayNames(language, {
type: "currency",
fallback: "code",
})
: undefined;
const options = CURRENCIES.map((currency) => ({
value: currency,
label: currencyDisplayNames ? currencyDisplayNames.of(currency)! : currency,
}));
options.sort((a, b) =>
caseInsensitiveStringCompare(a.label, b.label, language)
);
return options;
});
export const createCurrencyListEl = (language: string) => {
const list = document.createElement("datalist");
list.id = "currencies";
for (const currency of getCurrencyOptions(language)) {
const option = document.createElement("option");
option.value = currency.value;
option.innerText = currency.label;
list.appendChild(option);
}
return list;
};

View File

@ -1,4 +1,3 @@
import "@lit-labs/virtualizer";
import { mdiArrowDown, mdiArrowUp } from "@mdi/js";
import deepClone from "deep-clone-simple";
import {
@ -26,6 +25,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import { debounce } from "../../common/util/debounce";
import { nextRender } from "../../common/util/render-status";
import { haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
import { HomeAssistant } from "../../types";
import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
@ -184,6 +184,10 @@ export class HaDataTable extends LitElement {
public willUpdate(properties: PropertyValues) {
super.willUpdate(properties);
if (!this.hasUpdated) {
loadVirtualizer();
}
if (properties.has("columns")) {
this._filterable = Object.values(this.columns).some(
(column) => column.filterable
@ -486,7 +490,7 @@ export class HaDataTable extends LitElement {
}
private _memFilterData = memoizeOne(
async (
(
data: DataTableRowData[],
columns: SortableColumnContainer,
filter: string

View File

@ -9,7 +9,7 @@ type SortDataParamTypes = Parameters<SortDataType>;
let worker: Remote<Api> | undefined;
export const filterData = async (
export const filterData = (
data: FilterDataParamTypes[0],
columns: FilterDataParamTypes[1],
filter: FilterDataParamTypes[2]
@ -21,7 +21,7 @@ export const filterData = async (
return worker.filterData(data, columns, filter);
};
export const sortData = async (
export const sortData = (
data: SortDataParamTypes[0],
columns: SortDataParamTypes[1],
direction: SortDataParamTypes[2],

View File

@ -1,6 +1,6 @@
// To use comlink under ES5
import { expose } from "comlink";
import "proxy-polyfill";
import { expose } from "comlink";
import type {
ClonedDataTableColumnData,
DataTableRowData,

View File

@ -21,8 +21,7 @@ import {
subscribeEntityRegistry,
} from "../../data/entity_registry";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import { ValueChangedEvent, HomeAssistant } from "../../types";
import "../ha-icon-button";
import "../ha-svg-icon";
import "./ha-devices-picker";
@ -291,7 +290,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
this._areaPicker = !this._areaPicker;
}
private async _areaPicked(ev: PolymerChangedEvent<string>) {
private async _areaPicked(ev: ValueChangedEvent<string>) {
const value = ev.detail.value;
let selectedDevices = [];
const target = ev.target as any;

View File

@ -1,12 +1,15 @@
import { consume } from "@lit-labs/context";
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { fullEntitiesContext } from "../../data/context";
import {
DeviceAutomation,
deviceAutomationsEqual,
sortDeviceAutomations,
} from "../../data/device_automation";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { HomeAssistant } from "../../types";
import "../ha-select";
@ -30,6 +33,10 @@ export abstract class HaDeviceAutomationPicker<
// paper-listbox does not like changing things around.
@state() private _renderEmpty = false;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
protected get NO_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.actions.no_actions"
@ -44,6 +51,7 @@ export abstract class HaDeviceAutomationPicker<
private _localizeDeviceAutomation: (
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
automation: T
) => string;
@ -75,7 +83,7 @@ export abstract class HaDeviceAutomationPicker<
}
const idx = this._automations.findIndex((automation) =>
deviceAutomationsEqual(automation, this.value!)
deviceAutomationsEqual(this._entityReg, automation, this.value!)
);
if (idx === -1) {
@ -110,7 +118,11 @@ export abstract class HaDeviceAutomationPicker<
${this._automations.map(
(automation, idx) => html`
<mwc-list-item .value=${`${automation.device_id}_${idx}`}>
${this._localizeDeviceAutomation(this.hass, automation)}
${this._localizeDeviceAutomation(
this.hass,
this._entityReg,
automation
)}
</mwc-list-item>
`
)}
@ -161,7 +173,10 @@ export abstract class HaDeviceAutomationPicker<
}
private _setValue(automation: T) {
if (this.value && deviceAutomationsEqual(automation, this.value)) {
if (
this.value &&
deviceAutomationsEqual(this._entityReg, automation, this.value)
) {
return;
}
const value = { ...automation };

View File

@ -15,6 +15,7 @@ import {
computeDeviceName,
DeviceEntityLookup,
DeviceRegistryEntry,
getDeviceEntityLookup,
subscribeDeviceRegistry,
} from "../../data/device_registry";
import {
@ -22,8 +23,7 @@ import {
subscribeEntityRegistry,
} from "../../data/entity_registry";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import { ValueChangedEvent, HomeAssistant } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
@ -130,7 +130,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
];
}
const deviceEntityLookup: DeviceEntityLookup = {};
let deviceEntityLookup: DeviceEntityLookup = {};
if (
includeDomains ||
@ -138,15 +138,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
includeDeviceClasses ||
entityFilter
) {
for (const entity of entities) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {
deviceEntityLookup[entity.device_id] = [];
}
deviceEntityLookup[entity.device_id].push(entity);
}
deviceEntityLookup = getDeviceEntityLookup(entities);
}
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
@ -330,7 +322,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
return this.value || "";
}
private _deviceChanged(ev: PolymerChangedEvent<string>) {
private _deviceChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
let newValue = ev.detail.value;
@ -343,7 +335,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
}
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
private _openedChanged(ev: ValueChangedEvent<boolean>) {
this._opened = ev.detail.value;
}

View File

@ -1,8 +1,7 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import { ValueChangedEvent, HomeAssistant } from "../../types";
import "./ha-device-picker";
import type {
HaDevicePickerDeviceFilterFunc,
@ -108,7 +107,7 @@ class HaDevicesPicker extends LitElement {
this.value = devices;
}
private _deviceChanged(event: PolymerChangedEvent<string>) {
private _deviceChanged(event: ValueChangedEvent<string>) {
event.stopPropagation();
const curValue = (event.currentTarget as any).curValue;
const newValue = event.detail.value;
@ -126,7 +125,7 @@ class HaDevicesPicker extends LitElement {
}
}
private async _addDevice(event: PolymerChangedEvent<string>) {
private async _addDevice(event: ValueChangedEvent<string>) {
event.stopPropagation();
const toAdd = event.detail.value;
(event.currentTarget as any).value = "";

View File

@ -4,8 +4,7 @@ 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";
import type { HomeAssistant } from "../../types";
import type { ValueChangedEvent, HomeAssistant } from "../../types";
import "./ha-entity-picker";
import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
@ -151,7 +150,7 @@ class HaEntitiesPickerLight extends LitElement {
});
}
private _entityChanged(event: PolymerChangedEvent<string>) {
private _entityChanged(event: ValueChangedEvent<string>) {
event.stopPropagation();
const curValue = (event.currentTarget as any).curValue;
const newValue = event.detail.value;
@ -171,7 +170,7 @@ class HaEntitiesPickerLight extends LitElement {
);
}
private async _addEntity(event: PolymerChangedEvent<string>) {
private async _addEntity(event: ValueChangedEvent<string>) {
event.stopPropagation();
const toAdd = event.detail.value;
if (!toAdd) {

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