Merge pull request #9608 from home-assistant/dev

20210726.0
This commit is contained in:
Bram Kragten 2021-07-26 23:04:05 +02:00 committed by GitHub
commit a7a8aaa887
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
352 changed files with 25900 additions and 19409 deletions

View File

@ -35,55 +35,51 @@
"es6": true "es6": true
}, },
"rules": { "rules": {
"class-methods-use-this": 0, "class-methods-use-this": "off",
"new-cap": 0, "new-cap": "off",
"prefer-template": 0, "prefer-template": "off",
"object-shorthand": 0, "object-shorthand": "off",
"func-names": 0, "func-names": "off",
"prefer-arrow-callback": 0, "no-underscore-dangle": "off",
"no-underscore-dangle": 0, "strict": "off",
"strict": 0, "no-plusplus": "off",
"prefer-spread": 0, "no-bitwise": "error",
"no-plusplus": 0, "comma-dangle": "off",
"no-bitwise": 2, "vars-on-top": "off",
"comma-dangle": 0, "no-continue": "off",
"vars-on-top": 0, "no-param-reassign": "off",
"no-continue": 0, "no-multi-assign": "off",
"no-param-reassign": 0, "no-console": "error",
"no-multi-assign": 0, "radix": "off",
"no-console": 2, "no-alert": "off",
"radix": 0, "no-nested-ternary": "off",
"no-alert": 0, "prefer-destructuring": "off",
"no-return-await": 0,
"no-nested-ternary": 0,
"prefer-destructuring": 0,
"no-restricted-globals": [2, "event"], "no-restricted-globals": [2, "event"],
"prefer-promise-reject-errors": 0, "prefer-promise-reject-errors": "off",
"import/order": 0, "import/prefer-default-export": "off",
"import/prefer-default-export": 0, "import/no-default-export": "off",
"import/no-unresolved": 0, "import/no-unresolved": "off",
"import/no-cycle": 0, "import/no-cycle": "off",
"import/extensions": [ "import/extensions": [
2, "error",
"ignorePackages", "ignorePackages",
{ "ts": "never", "js": "never" } { "ts": "never", "js": "never" }
], ],
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"], "no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
"object-curly-newline": 0, "object-curly-newline": "off",
"default-case": 0, "default-case": "off",
"wc/no-self-class": 0, "wc/no-self-class": "off",
"no-shadow": 0, "no-shadow": "off",
"@typescript-eslint/camelcase": 0, "@typescript-eslint/camelcase": "off",
"@typescript-eslint/ban-ts-comment": 0, "@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-use-before-define": 0, "@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-non-null-assertion": 0, "@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": 0, "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-function-return-type": 0, "@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-shadow": ["error"], "@typescript-eslint/no-shadow": ["error"],
"@typescript-eslint/naming-convention": [ "@typescript-eslint/naming-convention": [
0, "off",
{ {
"selector": "default", "selector": "default",
"format": ["camelCase", "snake_case"], "format": ["camelCase", "snake_case"],
@ -101,9 +97,20 @@
"format": ["PascalCase"] "format": ["PascalCase"]
} }
], ],
"lit/attribute-value-entities": 0 "@typescript-eslint/no-unused-vars": "off",
}, "unused-imports/no-unused-vars": [
"plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"], "error",
"processor": "disable/disable", {
"ignorePatterns": ["src/resources/lit-virtualizer/*"] "vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_",
"ignoreRestSiblings": true
}
],
"unused-imports/no-unused-imports": "error",
"lit/attribute-value-entities": "off"
},
"plugins": ["disable", "unused-imports"],
"processor": "disable/disable"
} }

View File

@ -10,26 +10,21 @@ on:
- dev - dev
- master - master
env:
NODE_VERSION: 14
NODE_OPTIONS: --max_old_space_size=4096
jobs: jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setting up Node.js - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v1 uses: actions/setup-node@v2
with: with:
node-version: 12.x node-version: ${{ env.NODE_VERSION }}
- name: Get yarn cache path cache: yarn
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Fetching Yarn cache
uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies - name: Install dependencies
run: yarn install run: yarn install
env: env:
@ -42,51 +37,35 @@ jobs:
run: yarn run lint:types run: yarn run lint:types
- name: Run prettier - name: Run prettier
run: yarn run lint:prettier run: yarn run lint:prettier
- name: Check for duplicate dependencies
run: yarn dedupe --check
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setting up Node.js - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v1 uses: actions/setup-node@v2
with: with:
node-version: 12.x node-version: ${{ env.NODE_VERSION }}
- name: Get yarn cache path cache: yarn
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Fetching Yarn cache
uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies - name: Install dependencies
run: yarn install run: yarn install
env: env:
CI: true CI: true
- name: Run Mocha - name: Run Mocha
run: npm run mocha run: yarn run mocha
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [lint, test] needs: [lint, test]
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setting up Node.js - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v1 uses: actions/setup-node@v2
with: with:
node-version: 12.x node-version: ${{ env.NODE_VERSION }}
- name: Get yarn cache path cache: yarn
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Fetching Yarn cache
uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies - name: Install dependencies
run: yarn install run: yarn install
env: env:
@ -101,20 +80,11 @@ jobs:
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setting up Node.js - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v1 uses: actions/setup-node@v2
with: with:
node-version: 12.x node-version: ${{ env.NODE_VERSION }}
- name: Get yarn cache path cache: yarn
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Fetching Yarn cache
uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies - name: Install dependencies
run: yarn install run: yarn install
env: env:

View File

@ -4,26 +4,22 @@ on:
push: push:
branches: branches:
- dev - dev
env:
NODE_VERSION: 14
NODE_OPTIONS: --max_old_space_size=4096
jobs: jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setting up Node.js - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v1 uses: actions/setup-node@v2
with: with:
node-version: 12.x node-version: ${{ env.NODE_VERSION }}
- name: Get yarn cache path cache: yarn
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Fetching Yarn cache
uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies - name: Install dependencies
run: yarn install run: yarn install
env: env:

View File

@ -7,7 +7,8 @@ on:
env: env:
PYTHON_VERSION: 3.8 PYTHON_VERSION: 3.8
NODE_VERSION: 12.1 NODE_VERSION: 14
NODE_OPTIONS: --max_old_space_size=4096
jobs: jobs:
release: release:
@ -29,7 +30,15 @@ jobs:
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn
- name: Install dependencies
run: yarn install
- name: Download Translations
run: ./script/translations_download
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build and release package - name: Build and release package
run: | run: |
python3 -m pip install twine python3 -m pip install twine

View File

@ -1,8 +1,6 @@
name: Translations name: Translations
on: on:
schedule:
- cron: "30 0 * * *"
push: push:
branches: branches:
- dev - dev
@ -10,7 +8,7 @@ on:
- src/translations/en.json - src/translations/en.json
env: env:
NODE_VERSION: 12 NODE_VERSION: 14
jobs: jobs:
upload: upload:
@ -20,46 +18,8 @@ jobs:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
- name: Upload Translations - name: Upload Translations
run: | run: |
export LOKALISE_TOKEN="${{ secrets.LOKALISE_TOKEN }}" export LOKALISE_TOKEN="${{ secrets.LOKALISE_TOKEN }}"
./script/translations_upload_base ./script/translations_upload_base
download:
name: Download
needs: upload
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v2
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
- name: Download Translations
run: |
export LOKALISE_TOKEN="${{ secrets.LOKALISE_TOKEN }}"
npm install
./script/translations_download
- name: Initialize git
uses: home-assistant/actions/helpers/git-init@master
with:
name: GitHub Action
email: github-action@users.noreply.github.com
- name: Update translation
run: |
git add translations
git commit -am "Translation update"
git push

10
.gitignore vendored
View File

@ -8,9 +8,15 @@ hass_frontend/*
dist dist
# yarn # yarn
.yarn .yarn/*
yarn-error.log !.yarn/patches
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*
node_modules/* node_modules/*
yarn-error.log
npm-debug.log npm-debug.log
# Python stuff # Python stuff

2
.nvmrc
View File

@ -1 +1 @@
12.1 14

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,29 @@
diff --git a/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js
index d92179f7fd5315203f870a6963e871dc8ddf6c0c..362e284121b97e0fba0925225777aebc32e26b8d 100644
--- a/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js
+++ b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js
@@ -1,14 +1,15 @@
-let _ET, ET;
+let _ET;
+let ET;
export default async function EventTarget() {
- return ET || init();
+ return ET || init();
}
async function init() {
- _ET = window.EventTarget;
- try {
- new _ET();
- }
- catch (_a) {
- _ET = (await import('event-target-shim')).EventTarget;
- }
- return (ET = _ET);
+ _ET = window.EventTarget;
+ try {
+ new _ET();
+ } catch (_a) {
+ _ET = (await import("event-target-shim")).default.EventTarget;
+ }
+ return (ET = _ET);
}

View File

@ -0,0 +1,34 @@
diff --git a/lib/legacy/class.js b/lib/legacy/class.js
index aee2511be1cd9bf900ee552bc98190c1631c57c0..f2f499d68bf52034cac9c28307c99e8ce6b8417d 100644
--- a/lib/legacy/class.js
+++ b/lib/legacy/class.js
@@ -304,17 +304,23 @@ function GenerateClassFromInfo(info, Base, behaviors) {
// only proceed if the generated class' prototype has not been registered.
const generatedProto = PolymerGenerated.prototype;
if (!generatedProto.hasOwnProperty(JSCompiler_renameProperty('__hasRegisterFinished', generatedProto))) {
- generatedProto.__hasRegisterFinished = true;
+ // make sure legacy lifecycle is called on the *element*'s prototype
+ // and not the generated class prototype; if the element has been
+ // extended, these are *not* the same.
+ const proto = Object.getPrototypeOf(this);
+ // Only set flag when generated prototype itself is registered,
+ // as this element may be extended from, and needs to run `registered`
+ // on all behaviors on the subclass as well.
+ if (proto === generatedProto) {
+ generatedProto.__hasRegisterFinished = true;
+ }
// ensure superclass is registered first.
super._registered();
// copy properties onto the generated class lazily if we're optimizing,
- if (legacyOptimizations) {
+ if (legacyOptimizations && !Object.hasOwnProperty(generatedProto, '__hasCopiedProperties')) {
+ generatedProto.__hasCopiedProperties = true;
copyPropertiesToProto(generatedProto);
}
- // make sure legacy lifecycle is called on the *element*'s prototype
- // and not the generated class prototype; if the element has been
- // extended, these are *not* the same.
- const proto = Object.getPrototypeOf(this);
let list = lifecycle.beforeRegister;
if (list) {
for (let i=0; i < list.length; i++) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

55
.yarn/releases/yarn-2.4.2.cjs vendored Executable file

File diff suppressed because one or more lines are too long

9
.yarnrc.yml Normal file
View File

@ -0,0 +1,9 @@
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
spec: "@yarnpkg/plugin-typescript"
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-2.4.2.cjs

View File

@ -0,0 +1,170 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require("path");
// Currently only supports CommonJS modules, as require is synchronous. `import` would need babel running asynchronous.
module.exports = function inlineConstants(babel, options, cwd) {
const t = babel.types;
if (!Array.isArray(options.modules)) {
throw new TypeError(
"babel-plugin-inline-constants: expected a `modules` array to be passed"
);
}
if (options.resolveExtensions && !Array.isArray(options.resolveExtensions)) {
throw new TypeError(
"babel-plugin-inline-constants: expected `resolveExtensions` to be an array"
);
}
const ignoreModuleNotFound = options.ignoreModuleNotFound;
const resolveExtensions = options.resolveExtensions;
const hasRelativeModules = options.modules.some(
(module) => module.startsWith(".") || module.startsWith("/")
);
const modules = Object.fromEntries(
options.modules.map((module) => {
const absolute = module.startsWith(".")
? require.resolve(module, { paths: [cwd] })
: module;
// eslint-disable-next-line import/no-dynamic-require
return [absolute, require(absolute)];
})
);
const toLiteral = (value) => {
if (typeof value === "string") {
return t.stringLiteral(value);
}
if (typeof value === "number") {
return t.numericLiteral(value);
}
if (typeof value === "boolean") {
return t.booleanLiteral(value);
}
if (value === null) {
return t.nullLiteral();
}
throw new Error(
"babel-plugin-inline-constants: cannot handle non-literal `" + value + "`"
);
};
const resolveAbsolute = (value, state, resolveExtensionIndex) => {
if (!state.filename) {
throw new TypeError(
"babel-plugin-inline-constants: expected a `filename` to be set for files"
);
}
if (resolveExtensions && resolveExtensionIndex !== undefined) {
value += resolveExtensions[resolveExtensionIndex];
}
try {
return require.resolve(value, { paths: [path.dirname(state.filename)] });
} catch (error) {
if (
error.code === "MODULE_NOT_FOUND" &&
resolveExtensions &&
(resolveExtensionIndex === undefined ||
resolveExtensionIndex < resolveExtensions.length - 1)
) {
const resolveExtensionIdx = (resolveExtensionIndex || -1) + 1;
return resolveAbsolute(value, state, resolveExtensionIdx);
}
if (error.code === "MODULE_NOT_FOUND" && ignoreModuleNotFound) {
return undefined;
}
throw error;
}
};
const importDeclaration = (p, state) => {
if (p.node.type !== "ImportDeclaration") {
return;
}
const absolute =
hasRelativeModules && p.node.source.value.startsWith(".")
? resolveAbsolute(p.node.source.value, state)
: p.node.source.value;
if (!absolute || !(absolute in modules)) {
return;
}
const module = modules[absolute];
for (const specifier of p.node.specifiers) {
if (
specifier.type === "ImportDefaultSpecifier" &&
specifier.local &&
specifier.local.type === "Identifier"
) {
if (!("default" in module)) {
throw new Error(
"babel-plugin-inline-constants: cannot access default export from `" +
p.node.source.value +
"`"
);
}
const variableValue = toLiteral(module.default);
const variable = t.variableDeclarator(
t.identifier(specifier.local.name),
variableValue
);
p.insertBefore({
type: "VariableDeclaration",
kind: "const",
declarations: [variable],
});
} else if (
specifier.type === "ImportSpecifier" &&
specifier.imported &&
specifier.imported.type === "Identifier" &&
specifier.local &&
specifier.local.type === "Identifier"
) {
if (!(specifier.imported.name in module)) {
throw new Error(
"babel-plugin-inline-constants: cannot access `" +
specifier.imported.name +
"` from `" +
p.node.source.value +
"`"
);
}
const variableValue = toLiteral(module[specifier.imported.name]);
const variable = t.variableDeclarator(
t.identifier(specifier.local.name),
variableValue
);
p.insertBefore({
type: "VariableDeclaration",
kind: "const",
declarations: [variable],
});
} else {
throw new Error("Cannot handle specifier `" + specifier.type + "`");
}
}
p.remove();
};
return {
visitor: {
ImportDeclaration: importDeclaration,
},
};
};

View File

@ -18,7 +18,8 @@ module.exports.emptyPackages = ({ latestBuild }) =>
require.resolve("@polymer/paper-styles/default-theme.js"), require.resolve("@polymer/paper-styles/default-theme.js"),
// Loads stuff from a CDN // Loads stuff from a CDN
require.resolve("@polymer/font-roboto/roboto.js"), require.resolve("@polymer/font-roboto/roboto.js"),
require.resolve("@vaadin/vaadin-material-styles/font-roboto.js"), require.resolve("@vaadin/vaadin-material-styles/typography.js"),
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
// Compatibility not needed for latest builds // Compatibility not needed for latest builds
latestBuild && latestBuild &&
// wrapped in require.resolve so it blows up if file no longer exists // wrapped in require.resolve so it blows up if file no longer exists
@ -56,12 +57,23 @@ module.exports.babelOptions = ({ latestBuild }) => ({
"@babel/preset-env", "@babel/preset-env",
{ {
useBuiltIns: "entry", useBuiltIns: "entry",
corejs: "3.6", corejs: "3.15",
bugfixes: true,
}, },
], ],
"@babel/preset-typescript", "@babel/preset-typescript",
].filter(Boolean), ].filter(Boolean),
plugins: [ plugins: [
[
path.resolve(
paths.polymer_dir,
"build-scripts/babel-plugins/inline-constants-plugin.js"
),
{
modules: ["@mdi/js"],
ignoreModuleNotFound: true,
},
],
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2}) // Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
!latestBuild && [ !latestBuild && [
"@babel/plugin-proposal-object-rest-spread", "@babel/plugin-proposal-object-rest-spread",
@ -74,8 +86,14 @@ module.exports.babelOptions = ({ latestBuild }) => ({
"@babel/plugin-proposal-nullish-coalescing-operator", "@babel/plugin-proposal-nullish-coalescing-operator",
["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }], ["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],
["@babel/plugin-proposal-private-methods", { loose: true }], ["@babel/plugin-proposal-private-methods", { loose: true }],
["@babel/plugin-proposal-private-property-in-object", { loose: true }],
["@babel/plugin-proposal-class-properties", { loose: true }], ["@babel/plugin-proposal-class-properties", { loose: true }],
].filter(Boolean), ].filter(Boolean),
exclude: [
// \\ for Windows, / for Mac OS and Linux
/node_modules[\\/]core-js/,
/node_modules[\\/]webpack[\\/]buildin/,
],
}); });
const outputPath = (outputRoot, latestBuild) => const outputPath = (outputRoot, latestBuild) =>

View File

@ -47,8 +47,8 @@ gulp.task(
gulp.parallel("gen-icons-json", "build-translations"), gulp.parallel("gen-icons-json", "build-translations"),
"copy-static-app", "copy-static-app",
env.useRollup() ? "rollup-prod-app" : "webpack-prod-app", env.useRollup() ? "rollup-prod-app" : "webpack-prod-app",
...// Don't compress running tests // Don't compress running tests
(env.isTest() ? [] : ["compress-app"]), ...(env.isTest() ? [] : ["compress-app"]),
gulp.parallel( gulp.parallel(
"gen-pages-prod", "gen-pages-prod",
"gen-index-app-prod", "gen-index-app-prod",

View File

@ -2,7 +2,6 @@
const gulp = require("gulp"); const gulp = require("gulp");
const path = require("path"); const path = require("path");
const cpx = require("cpx");
const fs = require("fs-extra"); const fs = require("fs-extra");
const paths = require("../paths"); const paths = require("../paths");
@ -62,9 +61,12 @@ function copyLoaderJS(staticDir) {
function copyFonts(staticDir) { function copyFonts(staticDir) {
const staticPath = genStaticPath(staticDir); const staticPath = genStaticPath(staticDir);
// Local fonts // Local fonts
cpx.copySync( fs.copySync(
npmPath("roboto-fontface/fonts/roboto/*.woff2"), npmPath("roboto-fontface/fonts/roboto/"),
staticPath("fonts/roboto") staticPath("fonts/roboto/"),
{
filter: (src) => !src.includes(".") || src.endsWith(".woff2"),
}
); );
} }

View File

@ -90,7 +90,13 @@ gulp.task("webpack-watch-app", () => {
process.env.ES5 process.env.ES5
? bothBuilds(createAppConfig, { isProdBuild: false }) ? bothBuilds(createAppConfig, { isProdBuild: false })
: createAppConfig({ isProdBuild: false, latestBuild: true }) : createAppConfig({ isProdBuild: false, latestBuild: true })
).watch({ ignored: /build-translations/, poll: isWsl }, doneHandler()); ).watch(
{
ignored: /build-translations/,
poll: isWsl,
},
doneHandler()
);
gulp.watch( gulp.watch(
path.join(paths.translations_src, "en.json"), path.join(paths.translations_src, "en.json"),
gulp.series("build-translations", "copy-translations-app") gulp.series("build-translations", "copy-translations-app")

View File

@ -49,12 +49,16 @@ const createWebpackConfig = ({
test: /\.m?js$|\.ts$/, test: /\.m?js$|\.ts$/,
use: { use: {
loader: "babel-loader", loader: "babel-loader",
options: bundle.babelOptions({ latestBuild }), options: {
...bundle.babelOptions({ latestBuild }),
cacheDirectory: !isProdBuild,
cacheCompression: false,
},
}, },
}, },
{ {
test: /\.css$/, test: /\.css$/,
use: "raw-loader", type: "asset/source",
}, },
], ],
}, },
@ -66,6 +70,8 @@ const createWebpackConfig = ({
terserOptions: bundle.terserOptions(latestBuild), terserOptions: bundle.terserOptions(latestBuild),
}), }),
], ],
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
}, },
plugins: [ plugins: [
new WebpackManifestPlugin({ new WebpackManifestPlugin({
@ -112,16 +118,6 @@ const createWebpackConfig = ({
new RegExp(bundle.emptyPackages({ latestBuild }).join("|")), new RegExp(bundle.emptyPackages({ latestBuild }).join("|")),
path.resolve(paths.polymer_dir, "src/util/empty.js") path.resolve(paths.polymer_dir, "src/util/empty.js")
), ),
// We need to change the import of the polyfill for EventTarget, so we replace the polyfill file with our customized one
new webpack.NormalModuleReplacementPlugin(
new RegExp(
path.resolve(
paths.polymer_dir,
"src/resources/lit-virtualizer/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js"
)
),
path.resolve(paths.polymer_dir, "src/resources/EventTarget-ponyfill.js")
),
!isProdBuild && new LogStartCompilePlugin(), !isProdBuild && new LogStartCompilePlugin(),
].filter(Boolean), ].filter(Boolean),
resolve: { resolve: {
@ -134,15 +130,13 @@ const createWebpackConfig = ({
}, },
output: { output: {
filename: ({ chunk }) => { filename: ({ chunk }) => {
if (!isProdBuild || dontHash.has(chunk.name)) { if (!isProdBuild || isStatsBuild || dontHash.has(chunk.name)) {
return `${chunk.name}.js`; return `${chunk.name}.js`;
} }
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`; return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
}, },
chunkFilename: chunkFilename:
isProdBuild && !isStatsBuild isProdBuild && !isStatsBuild ? "[chunkhash:8].js" : "[id].chunk.js",
? "chunk.[chunkhash].js"
: "[name].chunk.js",
path: outputPath, path: outputPath,
publicPath, publicPath,
// To silence warning in worker plugin // To silence warning in worker plugin

View File

@ -139,7 +139,7 @@
Your authentication credentials or Home Assistant url are never sent Your authentication credentials or Home Assistant url are never sent
to the Cloud. You can validate this behavior in to the Cloud. You can validate this behavior in
<a <a
href="https://github.com/home-assistant/home-assistant-polymer/tree/dev/cast" href="https://github.com/home-assistant/frontend/tree/dev/cast"
target="_blank" target="_blank"
>the source code</a >the source code</a
>. >.

View File

@ -5,8 +5,8 @@ import {
import { castContext } from "../cast_context"; import { castContext } from "../cast_context";
export const castDemoLovelace: () => LovelaceConfig = () => { export const castDemoLovelace: () => LovelaceConfig = () => {
const touchSupported = castContext.getDeviceCapabilities() const touchSupported =
.touch_input_supported; castContext.getDeviceCapabilities().touch_input_supported;
return { return {
views: [ views: [
{ {

View File

@ -113,8 +113,7 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
on: "/assets/arsaboo/icons/light_bulb_on.png", on: "/assets/arsaboo/icons/light_bulb_on.png",
}, },
state_filter: { state_filter: {
on: on: "brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)",
"brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)",
off: "brightness(80%) saturate(0.8)", off: "brightness(80%) saturate(0.8)",
}, },
style: { style: {
@ -196,8 +195,7 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
on: "/assets/arsaboo/icons/light_bulb_on.png", on: "/assets/arsaboo/icons/light_bulb_on.png",
}, },
state_filter: { state_filter: {
on: on: "brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)",
"brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)",
off: "brightness(80%) saturate(0.8)", off: "brightness(80%) saturate(0.8)",
}, },
style: { style: {
@ -277,8 +275,7 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
on: "/assets/arsaboo/icons/light_bulb_on.png", on: "/assets/arsaboo/icons/light_bulb_on.png",
}, },
state_filter: { state_filter: {
on: on: "brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)",
"brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)",
off: "brightness(80%) saturate(0.8)", off: "brightness(80%) saturate(0.8)",
}, },
style: { style: {
@ -315,8 +312,7 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
on: "/assets/arsaboo/icons/light_bulb_on.png", on: "/assets/arsaboo/icons/light_bulb_on.png",
}, },
state_filter: { state_filter: {
on: on: "brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)",
"brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)",
off: "brightness(80%) saturate(0.8)", off: "brightness(80%) saturate(0.8)",
}, },
style: { style: {

View File

@ -12,9 +12,8 @@ export const demoConfigs: Array<() => Promise<DemoConfig>> = [
// eslint-disable-next-line import/no-mutable-exports // eslint-disable-next-line import/no-mutable-exports
export let selectedDemoConfigIndex = 0; export let selectedDemoConfigIndex = 0;
// eslint-disable-next-line import/no-mutable-exports // eslint-disable-next-line import/no-mutable-exports
export let selectedDemoConfig: Promise<DemoConfig> = demoConfigs[ export let selectedDemoConfig: Promise<DemoConfig> =
selectedDemoConfigIndex demoConfigs[selectedDemoConfigIndex]();
]();
export const setDemoConfig = async ( export const setDemoConfig = async (
hass: MockHomeAssistant, hass: MockHomeAssistant,

View File

@ -980,8 +980,7 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
icon: "mdi:account-off", icon: "mdi:account-off",
custom_ui_state_card: "state-card-custom-ui", custom_ui_state_card: "state-card-custom-ui",
templates: { templates: {
icon: icon: "if (state === 'on') return 'mdi:account'; else if (state === 'off') return 'mdi:account-off';\n",
"if (state === 'on') return 'mdi:account'; else if (state === 'off') return 'mdi:account-off';\n",
icon_color: icon_color:
"if (state === 'on') return 'rgb(56, 150, 56)'; else if (state === 'off') return 'rgb(249, 251, 255)';\n", "if (state === 'on') return 'rgb(56, 150, 56)'; else if (state === 'off') return 'rgb(249, 251, 255)';\n",
}, },
@ -1005,8 +1004,7 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
icon: "mdi:account-multiple-minus", icon: "mdi:account-multiple-minus",
custom_ui_state_card: "state-card-custom-ui", custom_ui_state_card: "state-card-custom-ui",
templates: { templates: {
icon: icon: "if (state === 'on') return 'mdi:account-group'; else if (state === 'off') return 'mdi:account-multiple-minus';\n",
"if (state === 'on') return 'mdi:account-group'; else if (state === 'off') return 'mdi:account-multiple-minus';\n",
icon_color: icon_color:
"if (state === 'on') return 'rgb(56, 150, 56)'; else if (state === 'off') return 'rgb(249, 251, 255)';\n", "if (state === 'on') return 'rgb(56, 150, 56)'; else if (state === 'off') return 'rgb(249, 251, 255)';\n",
}, },

View File

@ -19,7 +19,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
@property({ attribute: false }) public hass!: MockHomeAssistant; @property({ attribute: false }) public hass!: MockHomeAssistant;
@state() private _switching?: boolean; @state() private _switching = false;
private _hidden = localStorage.hide_demo_card; private _hidden = localStorage.hide_demo_card;
@ -27,12 +27,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
return this._hidden ? 0 : 2; return this._hidden ? 0 : 2;
} }
public setConfig( public setConfig(_config: LovelaceCardConfig) {}
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
config: LovelaceCardConfig
// eslint-disable-next-line @typescript-eslint/no-empty-function
) {}
protected render(): TemplateResult { protected render(): TemplateResult {
if (this._hidden) { if (this._hidden) {

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/ha-style";
import "../../src/resources/roboto"; import "../../src/resources/roboto";
import "../../src/resources/safari-14-attachshadow-patch"; import "../../src/resources/safari-14-attachshadow-patch";

View File

@ -67,14 +67,7 @@ const incrementalUnits = ["clients", "queries", "ads"];
export const mockHistory = (mockHass: MockHomeAssistant) => { export const mockHistory = (mockHass: MockHomeAssistant) => {
mockHass.mockAPI( mockHass.mockAPI(
new RegExp("history/period/.+"), new RegExp("history/period/.+"),
( (hass, _method, path, _parameters) => {
hass,
// @ts-ignore
method,
path,
// @ts-ignore
parameters
) => {
const params = parseQuery<HistoryQueryParams>(path.split("?")[1]); const params = parseQuery<HistoryQueryParams>(path.split("?")[1]);
const entities = params.filter_entity_id.split(","); const entities = params.filter_entity_id.split(",");
@ -95,7 +88,7 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
const numberState = Number(state.state); const numberState = Number(state.state);
if (isNaN(numberState)) { if (isNaN(numberState)) {
// eslint-disable-next-line // eslint-disable-next-line no-console
console.log( console.log(
"Ignoring state with unparsable state but with a unit", "Ignoring state with unparsable state but with a unit",
entityId, entityId,

View File

@ -10,10 +10,9 @@ export const mockLovelace = (
localizePromise: Promise<LocalizeFunc> localizePromise: Promise<LocalizeFunc>
) => { ) => {
hass.mockWS("lovelace/config", () => hass.mockWS("lovelace/config", () =>
Promise.all([ Promise.all([selectedDemoConfig, localizePromise]).then(
selectedDemoConfig, ([config, localize]) => config.lovelace(localize)
localizePromise, )
]).then(([config, localize]) => config.lovelace(localize))
); );
hass.mockWS("lovelace/config/save", () => Promise.resolve()); hass.mockWS("lovelace/config/save", () => Promise.resolve());

View File

@ -2,12 +2,12 @@ import { html, css, LitElement, TemplateResult } from "lit";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/trace/hat-script-graph"; import "../../../src/components/trace/hat-script-graph";
import "../../../src/components/trace/hat-trace-timeline"; import "../../../src/components/trace/hat-trace-timeline";
import { customElement, property, state } from "lit/decorators";
import { provideHass } from "../../../src/fake_data/provide_hass"; import { provideHass } from "../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import { DemoTrace } from "../data/traces/types"; import { DemoTrace } from "../data/traces/types";
import { basicTrace } from "../data/traces/basic_trace"; import { basicTrace } from "../data/traces/basic_trace";
import { motionLightTrace } from "../data/traces/motion-light-trace"; import { motionLightTrace } from "../data/traces/motion-light-trace";
import { customElement, property, state } from "lit/decorators";
const traces: DemoTrace[] = [basicTrace, motionLightTrace]; const traces: DemoTrace[] = [basicTrace, motionLightTrace];

View File

@ -2,6 +2,8 @@ import { html, css, LitElement, TemplateResult } from "lit";
import "../../../src/components/ha-formfield"; import "../../../src/components/ha-formfield";
import "../../../src/components/ha-switch"; import "../../../src/components/ha-switch";
import { classMap } from "lit/directives/class-map";
import { customElement, property, state } from "lit/decorators";
import { IntegrationManifest } from "../../../src/data/integration"; import { IntegrationManifest } from "../../../src/data/integration";
import { provideHass } from "../../../src/fake_data/provide_hass"; import { provideHass } from "../../../src/fake_data/provide_hass";
@ -15,8 +17,6 @@ import type {
} from "../../../src/panels/config/integrations/ha-config-integrations"; } from "../../../src/panels/config/integrations/ha-config-integrations";
import { DeviceRegistryEntry } from "../../../src/data/device_registry"; import { DeviceRegistryEntry } from "../../../src/data/device_registry";
import { EntityRegistryEntry } from "../../../src/data/entity_registry"; import { EntityRegistryEntry } from "../../../src/data/entity_registry";
import { classMap } from "lit/directives/class-map";
import { customElement, property, state } from "lit/decorators";
const createConfigEntry = ( const createConfigEntry = (
title: string, title: string,

View File

@ -2,7 +2,6 @@ import "@material/mwc-button";
import { ActionDetail } from "@material/mwc-list"; import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js"; import { mdiDotsVertical } from "@mdi/js";
import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea";
import { DEFAULT_SCHEMA, Type } from "js-yaml"; import { DEFAULT_SCHEMA, Type } from "js-yaml";
import { import {
css, css,
@ -329,10 +328,6 @@ class HassioAddonConfig extends LitElement {
color: var(--error-color); color: var(--error-color);
margin-top: 16px; margin-top: 16px;
} }
iron-autogrow-textarea {
width: 100%;
font-family: var(--code-font-family, monospace);
}
.syntaxerror { .syntaxerror {
color: var(--error-color); color: var(--error-color);
} }

View File

@ -2,6 +2,7 @@ import "../../../../src/components/ha-card";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-markdown"; import "../../../../src/components/ha-markdown";
import { customElement, property, state } from "lit/decorators";
import { import {
fetchHassioAddonDocumentation, fetchHassioAddonDocumentation,
HassioAddonDetails, HassioAddonDetails,
@ -12,7 +13,6 @@ import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style"; import { hassioStyle } from "../../resources/hassio-style";
import { Supervisor } from "../../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { customElement, property, state } from "lit/decorators";
@customElement("hassio-addon-documentation-tab") @customElement("hassio-addon-documentation-tab")
class HassioAddonDocumentationDashboard extends LitElement { class HassioAddonDocumentationDashboard extends LitElement {

View File

@ -1,6 +1,5 @@
import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-icon-button/mwc-icon-button";
import { mdiFolderUpload } from "@mdi/js"; import { mdiFolderUpload } from "@mdi/js";
import "@polymer/iron-input/iron-input";
import "@polymer/paper-input/paper-input-container"; import "@polymer/paper-input/paper-input-container";
import { html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";

View File

@ -61,10 +61,6 @@ class HassioMarkdownDialog extends LitElement {
app-toolbar [main-title] { app-toolbar [main-title] {
margin-left: 16px; margin-left: 16px;
} }
paper-checkbox {
display: block;
margin: 4px;
}
@media all and (max-width: 450px), all and (max-height: 500px) { @media all and (max-width: 450px), all and (max-height: 500px) {
ha-paper-dialog { ha-paper-dialog {
max-height: 100%; max-height: 100%;

View File

@ -41,7 +41,8 @@ const IP_VERSIONS = ["ipv4", "ipv6"];
@customElement("dialog-hassio-network") @customElement("dialog-hassio-network")
export class DialogHassioNetwork export class DialogHassioNetwork
extends LitElement extends LitElement
implements HassDialog<HassioNetworkDialogParams> { implements HassDialog<HassioNetworkDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor; @property({ attribute: false }) public supervisor!: Supervisor;
@ -492,7 +493,7 @@ export class DialogHassioNetwork
} }
private _handleRadioValueChangedAp(ev: CustomEvent): void { private _handleRadioValueChangedAp(ev: CustomEvent): void {
const value = ((ev.target as any).value as string) as const value = (ev.target as any).value as string as
| "open" | "open"
| "wep" | "wep"
| "wpa-psk"; | "wpa-psk";

View File

@ -161,9 +161,9 @@ class HassioRegistriesDialog extends LitElement {
public focus(): void { public focus(): void {
this.updateComplete.then(() => this.updateComplete.then(() =>
(this.shadowRoot?.querySelector( (
"[dialogInitialFocus]" this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
) as HTMLElement)?.focus() )?.focus()
); );
} }

View File

@ -161,9 +161,9 @@ class HassioRepositoriesDialog extends LitElement {
public focus() { public focus() {
this.updateComplete.then(() => this.updateComplete.then(() =>
(this.shadowRoot?.querySelector( (
"[dialogInitialFocus]" this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
) as HTMLElement)?.focus() )?.focus()
); );
} }

View File

@ -12,7 +12,8 @@ import { HassioSnapshotUploadDialogParams } from "./show-dialog-snapshot-upload"
@customElement("dialog-hassio-snapshot-upload") @customElement("dialog-hassio-snapshot-upload")
export class DialogHassioSnapshotUpload export class DialogHassioSnapshotUpload
extends LitElement extends LitElement
implements HassDialog<HassioSnapshotUploadDialogParams> { implements HassDialog<HassioSnapshotUploadDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: HassioSnapshotUploadDialogParams; @state() private _params?: HassioSnapshotUploadDialogParams;

View File

@ -30,7 +30,8 @@ import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot";
@customElement("dialog-hassio-snapshot") @customElement("dialog-hassio-snapshot")
class HassioSnapshotDialog class HassioSnapshotDialog
extends LitElement extends LitElement
implements HassDialog<HassioSnapshotDialogParams> { implements HassDialog<HassioSnapshotDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@state() private _error?: string; @state() private _error?: string;
@ -297,8 +298,7 @@ class HassioSnapshotDialog
if (window.location.href.includes("ui.nabu.casa")) { if (window.location.href.includes("ui.nabu.casa")) {
const confirm = await showConfirmationDialog(this, { const confirm = await showConfirmationDialog(this, {
title: "Potential slow download", title: "Potential slow download",
text: text: "Downloading snapshots over the Nabu Casa URL will take some time, it is recomended to use your local URL instead, do you want to continue?",
"Downloading snapshots over the Nabu Casa URL will take some time, it is recomended to use your local URL instead, do you want to continue?",
confirmText: "continue", confirmText: "continue",
dismissText: "cancel", dismissText: "cancel",
}); });

View File

@ -49,9 +49,9 @@ class DialogSupervisorUpdate extends LitElement {
public focus(): void { public focus(): void {
this.updateComplete.then(() => this.updateComplete.then(() =>
(this.shadowRoot?.querySelector( (
"[dialogInitialFocus]" this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
) as HTMLElement)?.focus() )?.focus()
); );
} }

View File

@ -121,7 +121,7 @@ export class HassioMain extends SupervisorBaseElement {
} }
} else { } else {
themeName = themeName =
((this.hass.selectedTheme as unknown) as string) || (this.hass.selectedTheme as unknown as string) ||
this.hass.themes.default_theme; this.hass.themes.default_theme;
} }

View File

@ -1,19 +1,19 @@
import { html, LitElement, TemplateResult } from "lit";
import { sanitizeUrl } from "@braintree/sanitize-url"; import { sanitizeUrl } from "@braintree/sanitize-url";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { navigate } from "../../src/common/navigate";
import { import {
createSearchParam, createSearchParam,
extractSearchParamsObject, extractSearchParamsObject,
} from "../../src/common/url/search-params"; } from "../../src/common/url/search-params";
import { Supervisor } from "../../src/data/supervisor/supervisor";
import "../../src/layouts/hass-error-screen"; import "../../src/layouts/hass-error-screen";
import { import {
ParamType, ParamType,
Redirect, Redirect,
Redirects, Redirects,
} from "../../src/panels/my/ha-panel-my"; } from "../../src/panels/my/ha-panel-my";
import { navigate } from "../../src/common/navigate";
import { HomeAssistant, Route } from "../../src/types"; import { HomeAssistant, Route } from "../../src/types";
import { Supervisor } from "../../src/data/supervisor/supervisor";
import { customElement, property, state } from "lit/decorators";
const REDIRECTS: Redirects = { const REDIRECTS: Redirects = {
supervisor: { supervisor: {

View File

@ -86,10 +86,8 @@ export class SupervisorBaseElement extends urlSyncMixin(
const unsubs = Object.keys(this._unsubs); const unsubs = Object.keys(this._unsubs);
for (const collection of Object.keys(this._collections)) { for (const collection of Object.keys(this._collections)) {
if (!unsubs.includes(collection)) { if (!unsubs.includes(collection)) {
this._unsubs[collection] = this._collections[ this._unsubs[collection] = this._collections[collection].subscribe(
collection (data) => this._updateSupervisor({ [collection]: data })
].subscribe((data) =>
this._updateSupervisor({ [collection]: data })
); );
} }
} }

View File

@ -1,4 +1,4 @@
module.exports = { module.exports = {
"*.{js,ts}": "eslint --fix", "*.{js,ts}": 'eslint --ignore-pattern "**/build-scripts/**/*.js" --fix',
"!(/translations)*.{js,ts,json,css,md,html}": "prettier --write", "!(/translations)*.{js,ts,json,css,md,html}": "prettier --write",
}; };

View File

@ -42,8 +42,9 @@
"@fullcalendar/daygrid": "5.1.0", "@fullcalendar/daygrid": "5.1.0",
"@fullcalendar/interaction": "5.1.0", "@fullcalendar/interaction": "5.1.0",
"@fullcalendar/list": "5.1.0", "@fullcalendar/list": "5.1.0",
"@lit-labs/virtualizer": "^0.6.0", "@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.6.0#./.yarn/patches/@lit-labs/virtualizer/0.7.0.patch",
"@material/chips": "=12.0.0-canary.1a8d06483.0", "@material/chips": "=12.0.0-canary.1a8d06483.0",
"@material/data-table": "=12.0.0-canary.1a8d06483.0",
"@material/mwc-button": "0.22.0-canary.cc04657a.0", "@material/mwc-button": "0.22.0-canary.cc04657a.0",
"@material/mwc-checkbox": "0.22.0-canary.cc04657a.0", "@material/mwc-checkbox": "0.22.0-canary.cc04657a.0",
"@material/mwc-circular-progress": "0.22.0-canary.cc04657a.0", "@material/mwc-circular-progress": "0.22.0-canary.cc04657a.0",
@ -62,33 +63,31 @@
"@material/top-app-bar": "=12.0.0-canary.1a8d06483.0", "@material/top-app-bar": "=12.0.0-canary.1a8d06483.0",
"@mdi/js": "5.9.55", "@mdi/js": "5.9.55",
"@mdi/svg": "5.9.55", "@mdi/svg": "5.9.55",
"@polymer/app-layout": "^3.0.2", "@polymer/app-layout": "^3.1.0",
"@polymer/app-storage": "^3.0.2",
"@polymer/iron-autogrow-textarea": "^3.0.1",
"@polymer/iron-flex-layout": "^3.0.1", "@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1", "@polymer/iron-icon": "^3.0.1",
"@polymer/iron-input": "^3.0.1", "@polymer/iron-input": "^3.0.1",
"@polymer/iron-overlay-behavior": "^3.0.2", "@polymer/iron-overlay-behavior": "^3.0.3",
"@polymer/iron-resizable-behavior": "^3.0.1", "@polymer/iron-resizable-behavior": "^3.0.1",
"@polymer/paper-checkbox": "^3.1.0", "@polymer/paper-checkbox": "^3.1.0",
"@polymer/paper-dialog": "^3.0.1", "@polymer/paper-dialog": "^3.0.1",
"@polymer/paper-dialog-behavior": "^3.0.1", "@polymer/paper-dialog-behavior": "^3.0.1",
"@polymer/paper-dialog-scrollable": "^3.0.1", "@polymer/paper-dialog-scrollable": "^3.0.1",
"@polymer/paper-dropdown-menu": "^3.0.1", "@polymer/paper-dropdown-menu": "^3.2.0",
"@polymer/paper-input": "^3.0.1", "@polymer/paper-input": "^3.2.1",
"@polymer/paper-item": "^3.0.1", "@polymer/paper-item": "^3.0.1",
"@polymer/paper-listbox": "^3.0.1", "@polymer/paper-listbox": "^3.0.1",
"@polymer/paper-menu-button": "^3.0.1", "@polymer/paper-menu-button": "^3.1.0",
"@polymer/paper-progress": "^3.0.1", "@polymer/paper-progress": "^3.0.1",
"@polymer/paper-radio-button": "^3.0.1", "@polymer/paper-radio-button": "^3.0.1",
"@polymer/paper-radio-group": "^3.0.1", "@polymer/paper-radio-group": "^3.0.1",
"@polymer/paper-ripple": "^3.0.1", "@polymer/paper-ripple": "^3.0.2",
"@polymer/paper-slider": "^3.0.1", "@polymer/paper-slider": "^3.0.1",
"@polymer/paper-styles": "^3.0.1", "@polymer/paper-styles": "^3.0.1",
"@polymer/paper-tabs": "^3.0.1", "@polymer/paper-tabs": "^3.1.0",
"@polymer/paper-toast": "^3.0.1", "@polymer/paper-toast": "^3.0.1",
"@polymer/paper-tooltip": "^3.0.1", "@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.1.0", "@polymer/polymer": "3.4.1",
"@thomasloven/round-slider": "0.5.2", "@thomasloven/round-slider": "0.5.2",
"@vaadin/vaadin-combo-box": "^5.0.10", "@vaadin/vaadin-combo-box": "^5.0.10",
"@vaadin/vaadin-date-picker": "^4.0.7", "@vaadin/vaadin-date-picker": "^4.0.7",
@ -96,10 +95,10 @@
"@vibrant/core": "^3.2.1-alpha.1", "@vibrant/core": "^3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
"@vue/web-component-wrapper": "^1.2.0", "@vue/web-component-wrapper": "^1.2.0",
"@webcomponents/webcomponentsjs": "^2.2.7", "@webcomponents/webcomponentsjs": "^2.2.10",
"chart.js": "^3.3.2", "chart.js": "^3.3.2",
"comlink": "^4.3.1", "comlink": "^4.3.1",
"core-js": "^3.6.5", "core-js": "^3.15.2",
"cropperjs": "^1.5.11", "cropperjs": "^1.5.11",
"date-fns": "^2.22.1", "date-fns": "^2.22.1",
"deep-clone-simple": "^1.1.1", "deep-clone-simple": "^1.1.1",
@ -107,7 +106,7 @@
"fecha": "^4.2.0", "fecha": "^4.2.0",
"fuse.js": "^6.0.0", "fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2", "google-timezones-json": "^1.0.2",
"hls.js": "^1.0.5", "hls.js": "^1.0.7",
"home-assistant-js-websocket": "^5.10.0", "home-assistant-js-websocket": "^5.10.0",
"idb-keyval": "^5.0.5", "idb-keyval": "^5.0.5",
"intl-messageformat": "^9.6.16", "intl-messageformat": "^9.6.16",
@ -123,7 +122,7 @@
"proxy-polyfill": "^0.3.1", "proxy-polyfill": "^0.3.1",
"punycode": "^2.1.1", "punycode": "^2.1.1",
"qrcode": "^1.4.4", "qrcode": "^1.4.4",
"regenerator-runtime": "^0.13.2", "regenerator-runtime": "^0.13.8",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"roboto-fontface": "^0.10.0", "roboto-fontface": "^0.10.0",
"sortablejs": "^1.10.2", "sortablejs": "^1.10.2",
@ -145,17 +144,17 @@
"xss": "^1.0.9" "xss": "^1.0.9"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.14.3", "@babel/core": "^7.14.6",
"@babel/plugin-external-helpers": "^7.12.13", "@babel/plugin-external-helpers": "^7.14.5",
"@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/plugin-proposal-decorators": "^7.13.15", "@babel/plugin-proposal-decorators": "^7.14.5",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
"@babel/plugin-proposal-object-rest-spread": "^7.13.8", "@babel/plugin-proposal-object-rest-spread": "^7.14.7",
"@babel/plugin-proposal-optional-chaining": "^7.13.12", "@babel/plugin-proposal-optional-chaining": "^7.14.5",
"@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/preset-env": "^7.14.2", "@babel/preset-env": "^7.14.7",
"@babel/preset-typescript": "^7.13.0", "@babel/preset-typescript": "^7.14.5",
"@koa/cors": "^3.1.0", "@koa/cors": "^3.1.0",
"@open-wc/dev-server-hmr": "^0.0.2", "@open-wc/dev-server-hmr": "^0.0.2",
"@rollup/plugin-babel": "^5.2.1", "@rollup/plugin-babel": "^5.2.1",
@ -172,22 +171,22 @@
"@types/mocha": "^8.2.2", "@types/mocha": "^8.2.2",
"@types/sortablejs": "^1.10.6", "@types/sortablejs": "^1.10.6",
"@types/webspeechapi": "^0.0.29", "@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^4.22.0", "@typescript-eslint/eslint-plugin": "^4.28.3",
"@typescript-eslint/parser": "^4.22.0", "@typescript-eslint/parser": "^4.28.3",
"@web/dev-server": "^0.0.24", "@web/dev-server": "^0.0.24",
"@web/dev-server-rollup": "^0.2.11", "@web/dev-server-rollup": "^0.2.11",
"babel-loader": "^8.1.0", "babel-loader": "^8.2.2",
"chai": "^4.3.4", "chai": "^4.3.4",
"cpx": "^1.5.0",
"del": "^4.0.0", "del": "^4.0.0",
"eslint": "^7.25.0", "eslint": "^7.30.0",
"eslint-config-airbnb-typescript": "^12.3.1", "eslint-config-airbnb-typescript": "^12.3.1",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-webpack": "^0.13.0", "eslint-import-resolver-webpack": "^0.13.1",
"eslint-plugin-disable": "^2.0.1", "eslint-plugin-disable": "^2.0.1",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.23.4",
"eslint-plugin-lit": "^1.3.0", "eslint-plugin-lit": "^1.5.1",
"eslint-plugin-prettier": "^3.4.0", "eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-unused-imports": "^1.1.2",
"eslint-plugin-wc": "^1.3.0", "eslint-plugin-wc": "^1.3.0",
"fancy-log": "^1.3.3", "fancy-log": "^1.3.3",
"fs-extra": "^7.0.1", "fs-extra": "^7.0.1",
@ -199,7 +198,7 @@
"gulp-zopfli-green": "^3.0.1", "gulp-zopfli-green": "^3.0.1",
"html-minifier": "^4.0.0", "html-minifier": "^4.0.0",
"husky": "^1.3.1", "husky": "^1.3.1",
"lint-staged": "^10.5.4", "lint-staged": "^11.0.1",
"lit-analyzer": "^1.2.1", "lit-analyzer": "^1.2.1",
"lodash.template": "^4.5.0", "lodash.template": "^4.5.0",
"magic-string": "^0.25.7", "magic-string": "^0.25.7",
@ -208,8 +207,7 @@
"mocha": "^8.4.0", "mocha": "^8.4.0",
"object-hash": "^2.0.3", "object-hash": "^2.0.3",
"open": "^7.0.4", "open": "^7.0.4",
"prettier": "^2.0.4", "prettier": "^2.3.2",
"raw-loader": "^2.0.0",
"require-dir": "^1.2.0", "require-dir": "^1.2.0",
"rollup": "^2.8.2", "rollup": "^2.8.2",
"rollup-plugin-string": "^3.0.0", "rollup-plugin-string": "^3.0.0",
@ -219,23 +217,22 @@
"sinon": "^11.0.0", "sinon": "^11.0.0",
"source-map-url": "^0.4.0", "source-map-url": "^0.4.0",
"systemjs": "^6.3.2", "systemjs": "^6.3.2",
"terser-webpack-plugin": "^5.1.2", "terser-webpack-plugin": "^5.1.4",
"ts-lit-plugin": "^1.2.1", "ts-lit-plugin": "^1.2.1",
"ts-mocha": "^8.0.0", "ts-mocha": "^8.0.0",
"typescript": "^4.2.4", "typescript": "^4.3.5",
"vinyl-buffer": "^1.0.1", "vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0", "vinyl-source-stream": "^2.0.0",
"webpack": "^5.24.1", "webpack": "^5.43.0",
"webpack-cli": "^4.5.0", "webpack-cli": "^4.7.2",
"webpack-dev-server": "^3.11.2", "webpack-dev-server": "^3.11.2",
"webpack-manifest-plugin": "^3.0.0", "webpack-manifest-plugin": "^3.1.1",
"workbox-build": "^6.1.5" "workbox-build": "^6.1.5"
}, },
"_comment": "Polymer fixed to 3.1 because 3.2 throws on logbook page", "_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
"_comment_2": "Fix in https://github.com/Polymer/polymer/pull/5569",
"resolutions": { "resolutions": {
"@polymer/polymer": "patch:@polymer/polymer@3.4.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@webcomponents/webcomponentsjs": "^2.2.10", "@webcomponents/webcomponentsjs": "^2.2.10",
"@polymer/polymer": "3.1.0",
"lit-html": "2.0.0-rc.3", "lit-html": "2.0.0-rc.3",
"lit-element": "3.0.0-rc.2" "lit-element": "3.0.0-rc.2"
}, },

View File

@ -2,9 +2,9 @@ from setuptools import setup, find_packages
setup( setup(
name="home-assistant-frontend", name="home-assistant-frontend",
version="20210707.0", version="20210726.0",
description="The Home Assistant frontend", description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer", url="https://github.com/home-assistant/frontend",
author="The Home Assistant Authors", author="The Home Assistant Authors",
author_email="hello@home-assistant.io", author_email="hello@home-assistant.io",
license="Apache-2.0", license="Apache-2.0",

View File

@ -59,7 +59,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
current: "hass:current-ac", current: "hass:current-ac",
carbon_dioxide: "mdi:molecule-co2", carbon_dioxide: "mdi:molecule-co2",
carbon_monoxide: "mdi:molecule-co", carbon_monoxide: "mdi:molecule-co",
energy: "hass:flash", energy: "hass:lightning-bolt",
humidity: "hass:water-percent", humidity: "hass:water-percent",
illuminance: "hass:brightness-5", illuminance: "hass:brightness-5",
temperature: "hass:thermometer", temperature: "hass:thermometer",

View File

@ -26,6 +26,9 @@ function checkToLocaleStringSupportsOptions() {
return false; return false;
} }
export const toLocaleDateStringSupportsOptions = checkToLocaleDateStringSupportsOptions(); export const toLocaleDateStringSupportsOptions =
export const toLocaleTimeStringSupportsOptions = checkToLocaleTimeStringSupportsOptions(); checkToLocaleDateStringSupportsOptions();
export const toLocaleStringSupportsOptions = checkToLocaleStringSupportsOptions(); export const toLocaleTimeStringSupportsOptions =
checkToLocaleTimeStringSupportsOptions();
export const toLocaleStringSupportsOptions =
checkToLocaleStringSupportsOptions();

View File

@ -82,14 +82,18 @@ class Storage {
const storage = new Storage(); const storage = new Storage();
export const LocalStorage = ( export const LocalStorage =
(
storageKey?: string, storageKey?: string,
property?: boolean, property?: boolean,
propertyOptions?: PropertyDeclaration propertyOptions?: PropertyDeclaration
): any => (clsElement: ClassElement) => { ): any =>
(clsElement: ClassElement) => {
const key = String(clsElement.key); const key = String(clsElement.key);
storageKey = storageKey || String(clsElement.key); storageKey = storageKey || String(clsElement.key);
const initVal = clsElement.initializer ? clsElement.initializer() : undefined; const initVal = clsElement.initializer
? clsElement.initializer()
: undefined;
storage.addFromStorage(storageKey); storage.addFromStorage(storageKey);

View File

@ -1,9 +1,9 @@
import type { LitElement } from "lit"; import type { LitElement } from "lit";
import type { ClassElement } from "../../types"; import type { ClassElement } from "../../types";
export const restoreScroll = (selector: string): any => ( export const restoreScroll =
element: ClassElement (selector: string): any =>
) => ({ (element: ClassElement) => ({
kind: "method", kind: "method",
placement: "prototype", placement: "prototype",
key: element.key, key: element.key,

View File

@ -43,7 +43,17 @@ export const domainIcon = (
: "hass:air-humidifier"; : "hass:air-humidifier";
case "lock": case "lock":
return compareState === "unlocked" ? "hass:lock-open" : "hass:lock"; switch (compareState) {
case "unlocked":
return "hass:lock-open";
case "jammed":
return "hass:lock-alert";
case "locking":
case "unlocking":
return "hass:lock-clock";
default:
return "hass:lock";
}
case "media_player": case "media_player":
return compareState === "playing" ? "hass:cast-connected" : "hass:cast"; return compareState === "playing" ? "hass:cast-connected" : "hass:cast";

View File

@ -4,7 +4,7 @@ import { DEFAULT_DOMAIN_ICON } from "../const";
import { computeDomain } from "./compute_domain"; import { computeDomain } from "./compute_domain";
import { domainIcon } from "./domain_icon"; import { domainIcon } from "./domain_icon";
export const stateIcon = (state: HassEntity) => { export const stateIcon = (state?: HassEntity) => {
if (!state) { if (!state) {
return DEFAULT_DOMAIN_ICON; return DEFAULT_DOMAIN_ICON;
} }

View File

@ -0,0 +1,2 @@
export const round = (value: number, precision = 2): number =>
Math.round(value * 10 ** precision) / 10 ** precision;

View File

@ -67,9 +67,11 @@ class SearchInput extends LitElement {
changedProps.has("noUnderline") && changedProps.has("noUnderline") &&
(this.noUnderline || changedProps.get("noUnderline") !== undefined) (this.noUnderline || changedProps.get("noUnderline") !== undefined)
) { ) {
(this._input.inputElement!.parentElement!.shadowRoot!.querySelector( (
this._input.inputElement!.parentElement!.shadowRoot!.querySelector(
"div.unfocused-line" "div.unfocused-line"
) as HTMLElement).style.display = this.noUnderline ? "none" : "block"; ) as HTMLElement
).style.display = this.noUnderline ? "none" : "block";
} }
} }

View File

@ -6,6 +6,7 @@
// 3. Disallow dates based on week number. // 3. Disallow dates based on week number.
// 4. Disallow dates only consisting of a year. // 4. Disallow dates only consisting of a year.
// https://regex101.com/r/kc5C14/3 // https://regex101.com/r/kc5C14/3
const regexp = /^\d{4}-(0[1-9]|1[0-2])-([12]\d|0[1-9]|3[01])[T| ](((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)(\8[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)$/; const regexp =
/^\d{4}-(0[1-9]|1[0-2])-([12]\d|0[1-9]|3[01])[T| ](((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)(\8[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)$/;
export const isTimestamp = (input: string): boolean => regexp.test(input); export const isTimestamp = (input: string): boolean => regexp.test(input);

View File

@ -64,18 +64,18 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
this.hass this.hass
.callService(this.domain, this.service, this.serviceData) .callService(this.domain, this.service, this.serviceData)
.then( .then(
function () { () => {
el.progress = false; el.progress = false;
el.$.progress.actionSuccess(); el.$.progress.actionSuccess();
eventData.success = true; eventData.success = true;
}, },
function () { () => {
el.progress = false; el.progress = false;
el.$.progress.actionError(); el.$.progress.actionError();
eventData.success = false; eventData.success = false;
} }
) )
.then(function () { .then(() => {
el.fire("hass-service-called", eventData); el.fire("hass-service-called", eventData);
}); });
} }

View File

@ -23,11 +23,11 @@ export default class HaChartBase extends LitElement {
@property({ attribute: "chart-type", reflect: true }) @property({ attribute: "chart-type", reflect: true })
public chartType: ChartType = "line"; public chartType: ChartType = "line";
@property({ attribute: false }) @property({ attribute: false }) public data: ChartData = { datasets: [] };
public data: ChartData = { datasets: [] };
@property({ attribute: false }) @property({ attribute: false }) public options?: ChartOptions;
public options?: ChartOptions;
@property({ attribute: false }) public plugins?: any[];
@state() private _tooltip?: Tooltip; @state() private _tooltip?: Tooltip;
@ -50,11 +50,14 @@ export default class HaChartBase extends LitElement {
if (!this.hasUpdated || !this.chart) { if (!this.hasUpdated || !this.chart) {
return; return;
} }
if (changedProps.has("plugins")) {
this.chart.destroy();
this._setupChart();
return;
}
if (changedProps.has("type")) { if (changedProps.has("type")) {
this.chart.config.type = this.chartType; this.chart.config.type = this.chartType;
} }
if (changedProps.has("data")) { if (changedProps.has("data")) {
this.chart.data = this.data; this.chart.data = this.data;
} }
@ -133,6 +136,14 @@ export default class HaChartBase extends LitElement {
)} )}
</ul> </ul>
</div> </div>
${this._tooltip.footer
? // footer has white-space: pre;
// prettier-ignore
html`<div class="footer">${Array.isArray(this._tooltip.footer)
? this._tooltip.footer.join("\n")
: this._tooltip.footer}
</div>`
: ""}
</div>` </div>`
: ""} : ""}
</div> </div>
@ -148,14 +159,7 @@ export default class HaChartBase extends LitElement {
type: this.chartType, type: this.chartType,
data: this.data, data: this.data,
options: this._createOptions(), options: this._createOptions(),
plugins: [ plugins: this._createPlugins(),
{
id: "afterRenderHook",
afterRender: (chart) => {
this._height = `${chart.height}px`;
},
},
],
}); });
} }
@ -177,6 +181,22 @@ export default class HaChartBase extends LitElement {
}; };
} }
private _createPlugins() {
return [
...(this.plugins || []),
{
id: "afterRenderHook",
afterRender: (chart) => {
this._height = `${chart.height}px`;
},
legend: {
...this.options?.plugins?.legend,
display: false,
},
},
];
}
private _legendClick(ev) { private _legendClick(ev) {
if (!this.chart) { if (!this.chart) {
return; return;
@ -302,6 +322,10 @@ export default class HaChartBase extends LitElement {
text-align: center; text-align: center;
font-weight: 500; font-weight: 500;
} }
.chartTooltip .footer {
font-weight: 500;
white-space: pre;
}
.chartTooltip .beforeBody { .chartTooltip .beforeBody {
text-align: center; text-align: center;
font-weight: 300; font-weight: 300;

View File

@ -6,12 +6,17 @@ import { LineChartEntity, LineChartState } from "../../data/history";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "./ha-chart-base"; import "./ha-chart-base";
const safeParseFloat = (value) => {
const parsed = parseFloat(value);
return isFinite(parsed) ? parsed : null;
};
class StateHistoryChartLine extends LitElement { class StateHistoryChartLine extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public data: LineChartEntity[] = []; @property({ attribute: false }) public data: LineChartEntity[] = [];
@property({ type: Boolean }) public names = false; @property() public names: boolean | Record<string, string> = false;
@property() public unit?: string; @property() public unit?: string;
@ -114,29 +119,23 @@ class StateHistoryChartLine extends LitElement {
private _generateData() { private _generateData() {
let colorIndex = 0; let colorIndex = 0;
const computedStyles = getComputedStyle(this); const computedStyles = getComputedStyle(this);
const deviceStates = this.data; const entityStates = this.data;
const datasets: ChartDataset<"line">[] = []; const datasets: ChartDataset<"line">[] = [];
let endTime: Date; let endTime: Date;
if (deviceStates.length === 0) { if (entityStates.length === 0) {
return; return;
} }
function safeParseFloat(value) {
const parsed = parseFloat(value);
return isFinite(parsed) ? parsed : null;
}
endTime = endTime =
this.endTime || this.endTime ||
// Get the highest date from the last date of each device // Get the highest date from the last date of each device
new Date( new Date(
Math.max.apply( Math.max(
null, ...entityStates.map((devSts) =>
deviceStates.map((devSts) =>
new Date( new Date(
devSts.states[devSts.states.length - 1].last_changed devSts.states[devSts.states.length - 1].last_changed
).getMilliseconds() ).getTime()
) )
) )
); );
@ -145,7 +144,7 @@ class StateHistoryChartLine extends LitElement {
} }
const names = this.names || {}; const names = this.names || {};
deviceStates.forEach((states) => { entityStates.forEach((states) => {
const domain = states.domain; const domain = states.domain;
const name = names[states.entity_id] || states.name; const name = names[states.entity_id] || states.name;
// array containing [value1, value2, etc] // array containing [value1, value2, etc]

View File

@ -79,7 +79,7 @@ export class StateHistoryChartTimeline extends LitElement {
@property({ attribute: false }) public data: TimelineEntity[] = []; @property({ attribute: false }) public data: TimelineEntity[] = [];
@property({ type: Boolean }) public names = false; @property() public names: boolean | Record<string, string> = false;
@property() public unit?: string; @property() public unit?: string;
@ -176,9 +176,9 @@ export class StateHistoryChartTimeline extends LitElement {
labelColor: (item) => ({ labelColor: (item) => ({
borderColor: (item.dataset.data[item.dataIndex] as TimeLineData) borderColor: (item.dataset.data[item.dataIndex] as TimeLineData)
.color!, .color!,
backgroundColor: (item.dataset.data[ backgroundColor: (
item.dataIndex item.dataset.data[item.dataIndex] as TimeLineData
] as TimeLineData).color!, ).color!,
}), }),
}, },
}, },

View File

@ -10,7 +10,6 @@ import { customElement, property } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { HistoryResult } from "../../data/history"; import { HistoryResult } from "../../data/history";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../ha-circular-progress";
import "./state-history-chart-line"; import "./state-history-chart-line";
import "./state-history-chart-timeline"; import "./state-history-chart-timeline";

View File

@ -0,0 +1,308 @@
import type {
ChartData,
ChartDataset,
ChartOptions,
ChartType,
} from "chart.js";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { getColorByIndex } from "../../common/color/colors";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { computeStateName } from "../../common/entity/compute_state_name";
import {
Statistics,
statisticsHaveType,
StatisticType,
} from "../../data/history";
import type { HomeAssistant } from "../../types";
import "./ha-chart-base";
@customElement("statistics-chart")
class StatisticsChart extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public statisticsData!: Statistics;
@property() public names: boolean | Record<string, string> = false;
@property({ attribute: false }) public endTime?: Date;
@property({ type: Array }) public statTypes: Array<StatisticType> = [
"sum",
"min",
"max",
"mean",
];
@property() public chartType: ChartType = "line";
@property({ type: Boolean }) public isLoadingData = false;
@state() private _chartData?: ChartData;
@state() private _chartOptions?: ChartOptions;
protected shouldUpdate(changedProps: PropertyValues): boolean {
return !(changedProps.size === 1 && changedProps.has("hass"));
}
public willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
this._createOptions();
}
if (changedProps.has("statisticsData")) {
this._generateData();
}
}
protected render(): TemplateResult {
if (!isComponentLoaded(this.hass, "history")) {
return html`<div class="info">
${this.hass.localize("ui.components.history_charts.history_disabled")}
</div>`;
}
if (this.isLoadingData && !this.statisticsData) {
return html`<div class="info">
${this.hass.localize(
"ui.components.statistics_charts.loading_statistics"
)}
</div>`;
}
if (!this.statisticsData || !Object.keys(this.statisticsData).length) {
return html`<div class="info">
${this.hass.localize(
"ui.components.statistics_charts.no_statistics_found"
)}
</div>`;
}
return html`
<ha-chart-base
.data=${this._chartData}
.options=${this._chartOptions}
.chartType=${this.chartType}
></ha-chart-base>
`;
}
private _createOptions() {
this._chartOptions = {
parsing: false,
animation: false,
scales: {
x: {
type: "time",
adapters: {
date: {
locale: this.hass.locale,
},
},
ticks: {
maxRotation: 0,
sampleSize: 5,
autoSkipPadding: 20,
major: {
enabled: true,
},
font: (context) =>
context.tick && context.tick.major
? ({ weight: "bold" } as any)
: {},
},
time: {
tooltipFormat: "datetimeseconds",
},
},
y: {
ticks: {
maxTicksLimit: 7,
},
},
},
plugins: {
tooltip: {
mode: "nearest",
callbacks: {
label: (context) => `${context.dataset.label}: ${context.parsed.y}`,
},
},
filler: {
propagate: true,
},
legend: {
display: false,
labels: {
usePointStyle: true,
},
},
},
hover: {
mode: "nearest",
},
elements: {
line: {
tension: 0.4,
borderWidth: 1.5,
},
point: {
hitRadius: 5,
},
},
};
}
private _generateData() {
let colorIndex = 0;
const statisticsData = Object.values(this.statisticsData);
const totalDataSets: ChartDataset<"line">[] = [];
let endTime: Date;
if (statisticsData.length === 0) {
return;
}
endTime =
this.endTime ||
// Get the highest date from the last date of each statistic
new Date(
Math.max(
...statisticsData.map((stats) =>
new Date(stats[stats.length - 1].start).getTime()
)
)
);
if (endTime > new Date()) {
endTime = new Date();
}
const names = this.names || {};
statisticsData.forEach((stats) => {
const firstStat = stats[0];
let name = names[firstStat.statistic_id];
if (!name) {
const entityState = this.hass.states[firstStat.statistic_id];
if (entityState) {
name = computeStateName(entityState);
} else {
name = firstStat.statistic_id;
}
}
// array containing [value1, value2, etc]
let prevValues: Array<number | null> | null = null;
// The datasets for the current statistic
const statDataSets: ChartDataset<"line">[] = [];
const pushData = (
timestamp: Date,
dataValues: Array<number | null> | null
) => {
if (!dataValues) return;
if (timestamp > endTime) {
// Drop datapoints that are after the requested endTime. This could happen if
// endTime is "now" and client time is not in sync with server time.
return;
}
statDataSets.forEach((d, i) => {
if (dataValues[i] === null && prevValues && prevValues[i] !== null) {
// null data values show up as gaps in the chart.
// If the current value for the dataset is null and the previous
// value of the data set is not null, then add an 'end' point
// to the chart for the previous value. Otherwise the gap will
// be too big. It will go from the start of the previous data
// value until the start of the next data value.
d.data.push({ x: timestamp.getTime(), y: prevValues[i]! });
}
d.data.push({ x: timestamp.getTime(), y: dataValues[i]! });
});
prevValues = dataValues;
};
const addDataSet = (
nameY: string,
step = false,
fill = false,
color?: string
) => {
if (!color) {
color = getColorByIndex(colorIndex);
colorIndex++;
}
statDataSets.push({
label: nameY,
fill: fill ? "origin" : false,
borderColor: color,
backgroundColor: color + "7F",
stepped: step ? "before" : false,
pointRadius: 0,
data: [],
});
};
const statTypes: this["statTypes"] = [];
this.statTypes.forEach((type) => {
if (statisticsHaveType(stats, type)) {
statTypes.push(type);
addDataSet(
`${name} (${this.hass.localize(
`ui.components.statistics_charts.statistic_types.${type}`
)})`,
false
);
}
});
// Process chart data.
stats.forEach((stat) => {
const dataValues: Array<number | null> = [];
statTypes.forEach((type) => {
const val = stat[type];
dataValues.push(val !== null ? Math.round(val * 100) / 100 : null);
});
const date = new Date(stat.start);
pushData(date, dataValues);
});
// Add an entry for final values
pushData(endTime, prevValues);
// Concat two arrays
Array.prototype.push.apply(totalDataSets, statDataSets);
});
this._chartData = {
datasets: totalDataSets,
};
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
min-height: 60px;
}
.info {
text-align: center;
line-height: 60px;
color: var(--secondary-text-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"statistics-chart": StatisticsChart;
}
}

View File

@ -19,10 +19,9 @@ export class TextBarElement extends BarElement {
draw(ctx) { draw(ctx) {
super.draw(ctx); super.draw(ctx);
const options = this.options as TextBaroptions; const options = this.options as TextBaroptions;
const { x, y, base, width, text } = (this as BarElement< const { x, y, base, width, text } = (
TextBarProps, this as BarElement<TextBarProps, TextBaroptions>
TextBaroptions ).getProps(["x", "y", "base", "width", "text"]);
>).getProps(["x", "y", "base", "width", "text"]);
if (!text) { if (!text) {
return; return;

View File

@ -1,4 +1,4 @@
import { Layout1d, scroll } from "../../resources/lit-virtualizer"; import { Layout1d, scroll } from "@lit-labs/virtualizer";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import { import {
css, css,
@ -10,10 +10,10 @@ import {
} from "lit"; } from "lit";
import { import {
customElement, customElement,
property,
state,
query,
eventOptions, eventOptions,
property,
query,
state,
} from "lit/decorators"; } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
@ -360,9 +360,8 @@ export class HaDataTable extends LitElement {
.rowId=${row[this.id]} .rowId=${row[this.id]}
@click=${this._handleRowClick} @click=${this._handleRowClick}
class="mdc-data-table__row ${classMap({ class="mdc-data-table__row ${classMap({
"mdc-data-table__row--selected": this._checkedRows.includes( "mdc-data-table__row--selected":
String(row[this.id]) this._checkedRows.includes(String(row[this.id])),
),
clickable: this.clickable, clickable: this.clickable,
})}" })}"
aria-selected=${ifDefined( aria-selected=${ifDefined(
@ -406,17 +405,15 @@ export class HaDataTable extends LitElement {
"mdc-data-table__cell--icon": Boolean( "mdc-data-table__cell--icon": Boolean(
column.type === "icon" column.type === "icon"
), ),
"mdc-data-table__cell--icon-button": Boolean( "mdc-data-table__cell--icon-button":
column.type === "icon-button" Boolean(column.type === "icon-button"),
),
grows: Boolean(column.grows), grows: Boolean(column.grows),
forceLTR: Boolean(column.forceLTR), forceLTR: Boolean(column.forceLTR),
})}" })}"
style=${column.width style=${column.width
? styleMap({ ? styleMap({
[column.grows [column.grows ? "minWidth" : "width"]:
? "minWidth" column.width,
: "width"]: column.width,
maxWidth: column.maxWidth maxWidth: column.maxWidth
? column.maxWidth ? column.maxWidth
: "", : "",

View File

@ -1,6 +1,6 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
@ -15,6 +15,7 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
@ -38,7 +39,6 @@ import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-svg-icon"; import "../ha-svg-icon";
import "./ha-devices-picker"; import "./ha-devices-picker";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
interface DevicesByArea { interface DevicesByArea {
[areaId: string]: AreaDevices; [areaId: string]: AreaDevices;
@ -52,20 +52,27 @@ interface AreaDevices {
const rowRenderer: ComboBoxLitRenderer<AreaDevices> = (item) => html`<style> const rowRenderer: ComboBoxLitRenderer<AreaDevices> = (item) => html`<style>
paper-item { paper-item {
width: 100%;
margin: -10px 0;
padding: 0; padding: 0;
margin: -10px;
margin-left: 0;
} }
mwc-icon-button { #content {
float: right; display: flex;
align-items: center;
} }
.devices { ha-svg-icon {
padding-left: 2px;
margin-right: -2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none; display: none;
} }
.devices.visible { :host([selected]) paper-item {
display: block; margin-left: 10px;
} }
</style> </style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-item> <paper-item>
<paper-item-body two-line=""> <paper-item-body two-line="">
<div class="name">${item.name}</div> <div class="name">${item.name}</div>

View File

@ -11,6 +11,8 @@ import {
} from "lit"; } from "lit";
import { customElement, property, state, query } from "lit/decorators"; import { customElement, property, state, query } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { mdiCheck } from "@mdi/js";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { compare } from "../../common/string/compare"; import { compare } from "../../common/string/compare";
@ -33,7 +35,6 @@ import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-combo-box"; import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box"; import type { HaComboBox } from "../ha-combo-box";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
interface Device { interface Device {
name: string; name: string;
@ -47,10 +48,27 @@ export type HaDevicePickerDeviceFilterFunc = (
const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<style> const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<style>
paper-item { paper-item {
margin: -10px 0;
padding: 0; padding: 0;
margin: -10px;
margin-left: 0;
}
#content {
display: flex;
align-items: center;
}
ha-svg-icon {
padding-left: 2px;
margin-right: -2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none;
}
:host([selected]) paper-item {
margin-left: 10px;
} }
</style> </style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-item> <paper-item>
<paper-item-body two-line> <paper-item-body two-line>
${item.name} ${item.name}

View File

@ -12,7 +12,7 @@ import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
class HaEntitiesPickerLight extends LitElement { class HaEntitiesPickerLight extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property() public value?: string[]; @property({ type: Array }) public value?: string[];
/** /**
* Show entities from specific domains. * Show entities from specific domains.
@ -30,6 +30,22 @@ class HaEntitiesPickerLight extends LitElement {
@property({ type: Array, attribute: "exclude-domains" }) @property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[]; public excludeDomains?: string[];
/**
* Show only entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
/**
* Show only entities with these unit of measuments.
* @type {Array}
* @attr include-unit-of-measurement
*/
@property({ type: Array, attribute: "include-unit-of-measurement" })
public includeUnitOfMeasurement?: string[];
@property({ attribute: "picked-entity-label" }) @property({ attribute: "picked-entity-label" })
public pickedEntityLabel?: string; public pickedEntityLabel?: string;
@ -51,6 +67,8 @@ class HaEntitiesPickerLight extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.includeDomains=${this.includeDomains} .includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains} .excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this._entityFilter} .entityFilter=${this._entityFilter}
.value=${entityId} .value=${entityId}
.label=${this.pickedEntityLabel} .label=${this.pickedEntityLabel}
@ -64,6 +82,8 @@ class HaEntitiesPickerLight extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.includeDomains=${this.includeDomains} .includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains} .excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this._entityFilter} .entityFilter=${this._entityFilter}
.label=${this.pickEntityLabel} .label=${this.pickEntityLabel}
@value-changed=${this._addEntity} @value-changed=${this._addEntity}
@ -81,11 +101,11 @@ class HaEntitiesPickerLight extends LitElement {
} }
private async _updateEntities(entities) { private async _updateEntities(entities) {
this.value = entities;
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: entities, value: entities,
}); });
this.value = entities;
} }
private _entityChanged(event: PolymerChangedEvent<string>) { private _entityChanged(event: PolymerChangedEvent<string>) {
@ -98,15 +118,14 @@ class HaEntitiesPickerLight extends LitElement {
) { ) {
return; return;
} }
if (newValue === "") { const currentEntities = this._currentEntities;
this._updateEntities( if (!newValue || currentEntities.includes(newValue)) {
this._currentEntities.filter((ent) => ent !== curValue) this._updateEntities(currentEntities.filter((ent) => ent !== curValue));
); return;
} else {
this._updateEntities(
this._currentEntities.map((ent) => (ent === curValue ? newValue : ent))
);
} }
this._updateEntities(
currentEntities.map((ent) => (ent === curValue ? newValue : ent))
);
} }
private async _addEntity(event: PolymerChangedEvent<string>) { private async _addEntity(event: PolymerChangedEvent<string>) {

View File

@ -1,5 +1,5 @@
import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light"; import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
@ -25,10 +25,27 @@ export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
const rowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style> const rowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style>
paper-item { paper-item {
margin: -5px -10px;
padding: 0; padding: 0;
margin: -10px;
margin-left: 0;
}
#content {
display: flex;
align-items: center;
}
ha-svg-icon {
padding-left: 2px;
margin-right: -2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none;
}
:host([selected]) paper-item {
margin-left: 10px;
} }
</style> </style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-item>${formatAttributeName(item)}</paper-item>`; <paper-item>${formatAttributeName(item)}</paper-item>`;
@customElement("ha-entity-attribute-picker") @customElement("ha-entity-attribute-picker")

View File

@ -1,5 +1,5 @@
import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
@ -28,10 +28,25 @@ export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
const rowRenderer: ComboBoxLitRenderer<HassEntity> = (item) => html`<style> const rowRenderer: ComboBoxLitRenderer<HassEntity> = (item) => html`<style>
paper-icon-item { paper-icon-item {
margin: -10px;
padding: 0; padding: 0;
margin: -8px;
}
#content {
display: flex;
align-items: center;
}
ha-svg-icon {
padding-left: 2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none;
}
:host([selected]) paper-icon-item {
margin-left: 0;
} }
</style> </style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-icon-item> <paper-icon-item>
<state-badge slot="item-icon" .stateObj=${item}></state-badge> <state-badge slot="item-icon" .stateObj=${item}></state-badge>
<paper-item-body two-line=""> <paper-item-body two-line="">
@ -42,6 +57,8 @@ const rowRenderer: ComboBoxLitRenderer<HassEntity> = (item) => html`<style>
@customElement("ha-entity-picker") @customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement { export class HaEntityPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public autofocus = false; @property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled?: boolean; @property({ type: Boolean }) public disabled?: boolean;
@ -49,8 +66,6 @@ export class HaEntityPicker extends LitElement {
@property({ type: Boolean, attribute: "allow-custom-entity" }) @property({ type: Boolean, attribute: "allow-custom-entity" })
public allowCustomEntity; public allowCustomEntity;
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public label?: string; @property() public label?: string;
@property() public value?: string; @property() public value?: string;
@ -79,6 +94,14 @@ export class HaEntityPicker extends LitElement {
@property({ type: Array, attribute: "include-device-classes" }) @property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[]; public includeDeviceClasses?: string[];
/**
* Show only entities with these unit of measuments.
* @type {Array}
* @attr include-unit-of-measurement
*/
@property({ type: Array, attribute: "include-unit-of-measurement" })
public includeUnitOfMeasurement?: string[];
@property() public entityFilter?: HaEntityPickerEntityFilterFunc; @property() public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Boolean }) public hideClearIcon = false; @property({ type: Boolean }) public hideClearIcon = false;
@ -110,7 +133,8 @@ export class HaEntityPicker extends LitElement {
includeDomains: this["includeDomains"], includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"], excludeDomains: this["excludeDomains"],
entityFilter: this["entityFilter"], entityFilter: this["entityFilter"],
includeDeviceClasses: this["includeDeviceClasses"] includeDeviceClasses: this["includeDeviceClasses"],
includeUnitOfMeasurement: this["includeUnitOfMeasurement"]
) => { ) => {
let states: HassEntity[] = []; let states: HassEntity[] = [];
@ -143,6 +167,18 @@ export class HaEntityPicker extends LitElement {
); );
} }
if (includeUnitOfMeasurement) {
states = states.filter(
(stateObj) =>
// We always want to include the entity of the current value
stateObj.entity_id === this.value ||
(stateObj.attributes.unit_of_measurement &&
includeUnitOfMeasurement.includes(
stateObj.attributes.unit_of_measurement
))
);
}
if (entityFilter) { if (entityFilter) {
states = states.filter( states = states.filter(
(stateObj) => (stateObj) =>
@ -184,7 +220,7 @@ export class HaEntityPicker extends LitElement {
return !(!changedProps.has("_opened") && this._opened); return !(!changedProps.has("_opened") && this._opened);
} }
protected updated(changedProps: PropertyValues) { public willUpdate(changedProps: PropertyValues) {
if (!this._initedStates || (changedProps.has("_opened") && this._opened)) { if (!this._initedStates || (changedProps.has("_opened") && this._opened)) {
this._states = this._getStates( this._states = this._getStates(
this._opened, this._opened,
@ -192,23 +228,24 @@ export class HaEntityPicker extends LitElement {
this.includeDomains, this.includeDomains,
this.excludeDomains, this.excludeDomains,
this.entityFilter, this.entityFilter,
this.includeDeviceClasses this.includeDeviceClasses,
this.includeUnitOfMeasurement
); );
if (this._initedStates) {
(this.comboBox as any).filteredItems = this._states; (this.comboBox as any).filteredItems = this._states;
}
this._initedStates = true; this._initedStates = true;
} }
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html` return html`
<vaadin-combo-box-light <vaadin-combo-box-light
item-value-path="entity_id" item-value-path="entity_id"
item-label-path="entity_id" item-label-path="entity_id"
.value=${this._value} .value=${this._value}
.allowCustomValue=${this.allowCustomEntity} .allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._states}
${comboBoxRenderer(rowRenderer)} ${comboBoxRenderer(rowRenderer)}
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}

View File

@ -0,0 +1,256 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiCheck } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import { HassEntity } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeStateName } from "../../common/entity/compute_state_name";
import { compare } from "../../common/string/compare";
import { getStatisticIds, StatisticsMetaData } from "../../data/history";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-svg-icon";
import "./state-badge";
// vaadin-combo-box-item
const rowRenderer: ComboBoxLitRenderer<{
id: string;
name: string;
state?: HassEntity;
}> = (item) => html`<style>
paper-icon-item {
padding: 0;
margin: -8px;
}
#content {
display: flex;
align-items: center;
}
ha-svg-icon {
padding-left: 2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none;
}
:host([selected]) paper-icon-item {
margin-left: 0;
}
</style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-icon-item>
<state-badge slot="item-icon" .stateObj=${item.state}></state-badge>
<paper-item-body two-line="">
${item.name}
<span secondary>${item.id}</span>
</paper-item-body>
</paper-icon-item>`;
@customElement("ha-statistic-picker")
export class HaStatisticPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property({ attribute: "statistic-types" })
public statisticTypes?: "mean" | "sum";
@property({ type: Array }) public statisticIds?: StatisticsMetaData[];
@property({ type: Boolean }) public disabled?: boolean;
/**
* Show only statistics with these unit of measuments.
* @type {Array}
* @attr include-unit-of-measurement
*/
@property({ type: Array, attribute: "include-unit-of-measurement" })
public includeUnitOfMeasurement?: string[];
/**
* Show only statistics on entities.
* @type {Boolean}
* @attr entities-only
*/
@property({ type: Boolean, attribute: "entities-only" })
public entitiesOnly = false;
@state() private _opened?: boolean;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
private _init = false;
private _getStatistics = memoizeOne(
(
statisticIds: StatisticsMetaData[],
includeUnitOfMeasurement?: string[],
entitiesOnly?: boolean
): Array<{ id: string; name: string; state?: HassEntity }> => {
if (!statisticIds.length) {
return [
{
id: "",
name: this.hass.localize(
"ui.components.statistics-picker.no_statistics"
),
},
];
}
if (includeUnitOfMeasurement) {
statisticIds = statisticIds.filter((meta) =>
includeUnitOfMeasurement.includes(meta.unit_of_measurement)
);
}
const output: Array<{
id: string;
name: string;
state?: HassEntity;
}> = [];
statisticIds.forEach((meta) => {
const entityState = this.hass.states[meta.statistic_id];
if (!entityState) {
if (!entitiesOnly) {
output.push({ id: meta.statistic_id, name: meta.statistic_id });
}
return;
}
output.push({
id: meta.statistic_id,
name: computeStateName(entityState),
state: entityState,
});
});
if (output.length === 1) {
return output;
}
return output.sort((a, b) => compare(a.name || "", b.name || ""));
}
);
public open() {
this.comboBox?.open();
}
public focus() {
this.comboBox?.focus();
}
public willUpdate(changedProps: PropertyValues) {
if (
(!this.hasUpdated && !this.statisticIds) ||
changedProps.has("statisticTypes")
) {
this._getStatisticIds();
}
if (
(!this._init && this.statisticIds) ||
(changedProps.has("_opened") && this._opened)
) {
this._init = true;
if (this.hasUpdated) {
(this.comboBox as any).items = this._getStatistics(
this.statisticIds!,
this.includeUnitOfMeasurement,
this.entitiesOnly
);
} else {
this.updateComplete.then(() => {
(this.comboBox as any).items = this._getStatistics(
this.statisticIds!,
this.includeUnitOfMeasurement,
this.entitiesOnly
);
});
}
}
}
protected render(): TemplateResult {
return html`
<ha-combo-box
.hass=${this.hass}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.statistic-picker.statistic")
: this.label}
.value=${this._value}
.renderer=${rowRenderer}
.disabled=${this.disabled}
item-value-path="id"
item-id-path="id"
item-label-path="name"
@opened-changed=${this._openedChanged}
@value-changed=${this._statisticChanged}
></ha-combo-box>
`;
}
private async _getStatisticIds() {
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
}
private get _value() {
return this.value || "";
}
private _statisticChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
const newValue = ev.detail.value;
if (newValue !== this._value) {
this._setValue(newValue);
}
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}, 0);
}
static get styles(): CSSResultGroup {
return css`
paper-input > mwc-icon-button {
--mdc-icon-button-size: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-statistic-picker": HaStatisticPicker;
}
}

View File

@ -0,0 +1,110 @@
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { PolymerChangedEvent } from "../../polymer-types";
import type { HomeAssistant } from "../../types";
import "./ha-statistic-picker";
@customElement("ha-statistics-picker")
class HaStatisticsPicker extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Array }) public value?: string[];
@property({ type: Array }) public statisticIds?: string[];
@property({ attribute: "statistic-types" })
public statisticTypes?: "mean" | "sum";
@property({ attribute: "picked-statistic-label" })
public pickedStatisticLabel?: string;
@property({ attribute: "pick-statistic-label" })
public pickStatisticLabel?: string;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
const currentStatistics = this._currentStatistics;
return html`
${currentStatistics.map(
(statisticId) => html`
<div>
<ha-statistic-picker
.curValue=${statisticId}
.hass=${this.hass}
.value=${statisticId}
.statisticTypes=${this.statisticTypes}
.statisticIds=${this.statisticIds}
.label=${this.pickedStatisticLabel}
@value-changed=${this._statisticChanged}
></ha-statistic-picker>
</div>
`
)}
<div>
<ha-statistic-picker
.hass=${this.hass}
.statisticTypes=${this.statisticTypes}
.statisticIds=${this.statisticIds}
.label=${this.pickStatisticLabel}
@value-changed=${this._addStatistic}
></ha-statistic-picker>
</div>
`;
}
private get _currentStatistics() {
return this.value || [];
}
private async _updateStatistics(entities) {
this.value = entities;
fireEvent(this, "value-changed", {
value: entities,
});
}
private _statisticChanged(event: PolymerChangedEvent<string>) {
event.stopPropagation();
const oldValue = (event.currentTarget as any).curValue;
const newValue = event.detail.value;
if (newValue === oldValue) {
return;
}
const currentStatistics = this._currentStatistics;
if (!newValue || currentStatistics.includes(newValue)) {
this._updateStatistics(
currentStatistics.filter((ent) => ent !== oldValue)
);
return;
}
this._updateStatistics(
currentStatistics.map((ent) => (ent === oldValue ? newValue : ent))
);
}
private async _addStatistic(event: PolymerChangedEvent<string>) {
event.stopPropagation();
const toAdd = event.detail.value;
(event.currentTarget as any).value = "";
if (!toAdd) {
return;
}
const currentEntities = this._currentStatistics;
if (currentEntities.includes(toAdd)) {
return;
}
this._updateStatistics([...currentEntities, toAdd]);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-statistics-picker": HaStatisticsPicker;
}
}

View File

@ -1,5 +1,7 @@
import { mdiCheck } from "@mdi/js";
import { html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state, query } from "lit/decorators"; import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../common/config/is_component_loaded"; import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { compare } from "../common/string/compare"; import { compare } from "../common/string/compare";
@ -9,14 +11,33 @@ import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { PolymerChangedEvent } from "../polymer-types"; import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { HaComboBox } from "./ha-combo-box"; import { HaComboBox } from "./ha-combo-box";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) => html`<style> const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) => html`<style>
paper-item { paper-item {
margin: -10px 0;
padding: 0; padding: 0;
margin: -10px;
margin-left: 0px;
}
#content {
display: flex;
align-items: center;
}
:host([selected]) paper-item {
margin-left: 0;
}
ha-svg-icon {
padding-left: 2px;
margin-right: -2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none;
}
:host([selected]) paper-icon-item {
margin-left: 0;
} }
</style> </style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-item> <paper-item>
<paper-item-body two-line> <paper-item-body two-line>
${item.name} ${item.name}

View File

@ -1,5 +1,5 @@
import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
@ -48,13 +48,27 @@ const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (
item item
) => html`<style> ) => html`<style>
paper-item { paper-item {
margin: -10px 0;
padding: 0; padding: 0;
margin: -10px;
margin-left: 0;
} }
paper-item.add-new { #content {
font-weight: 500; display: flex;
align-items: center;
}
ha-svg-icon {
padding-left: 2px;
margin-right: -2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none;
}
:host([selected]) paper-item {
margin-left: 10px;
} }
</style> </style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-item class=${classMap({ "add-new": item.area_id === "add_new" })}> <paper-item class=${classMap({ "add-new": item.area_id === "add_new" })}>
<paper-item-body two-line>${item.name}</paper-item-body> <paper-item-body two-line>${item.name}</paper-item-body>
</paper-item>`; </paper-item>`;

View File

@ -2,6 +2,7 @@ import "@material/mwc-menu";
import type { Corner, Menu } from "@material/mwc-menu"; import type { Corner, Menu } from "@material/mwc-menu";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
@customElement("ha-button-menu") @customElement("ha-button-menu")
export class HaButtonMenu extends LitElement { export class HaButtonMenu extends LitElement {
@property() public corner: Corner = "TOP_START"; @property() public corner: Corner = "TOP_START";

View File

@ -1,12 +1,15 @@
import { Dialog } from "@material/mwc-dialog"; import { Dialog } from "@material/mwc-dialog";
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import { css, CSSResultGroup, html } from "lit"; import { css, CSSResultGroup, html, TemplateResult } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { computeRTLDirection } from "../common/util/compute_rtl"; import { computeRTLDirection } from "../common/util/compute_rtl";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-icon-button"; import "./ha-icon-button";
export const createCloseHeading = (hass: HomeAssistant, title: string) => html` export const createCloseHeading = (
hass: HomeAssistant,
title: string | TemplateResult
) => html`
<span class="header_title">${title}</span> <span class="header_title">${title}</span>
<mwc-icon-button <mwc-icon-button
aria-label=${hass.localize("ui.dialogs.generic.close")} aria-label=${hass.localize("ui.dialogs.generic.close")}

View File

@ -43,9 +43,9 @@ export class HaFileUpload extends LitElement {
protected updated(changedProperties: PropertyValues) { protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("_drag") && !this.uploading) { if (changedProperties.has("_drag") && !this.uploading) {
(this.shadowRoot!.querySelector( (
"paper-input-container" this.shadowRoot!.querySelector("paper-input-container") as any
) as any)._setFocused(this._drag); )._setFocused(this._drag);
} }
} }

View File

@ -1,12 +1,11 @@
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import "@polymer/paper-slider/paper-slider";
import type { PaperSliderElement } from "@polymer/paper-slider/paper-slider";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { HaCheckbox } from "../ha-checkbox"; import { HaCheckbox } from "../ha-checkbox";
import "../ha-slider"; import "../ha-slider";
import type { HaSlider } from "../ha-slider";
import { import {
HaFormElement, HaFormElement,
HaFormIntegerData, HaFormIntegerData,
@ -88,9 +87,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
} }
private _valueChanged(ev: Event) { private _valueChanged(ev: Event) {
const value = Number( const value = Number((ev.target as PaperInputElement | HaSlider).value);
(ev.target as PaperInputElement | PaperSliderElement).value
);
if (this._value === value) { if (this._value === value) {
return; return;
} }

View File

@ -94,8 +94,9 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
protected firstUpdated() { protected firstUpdated() {
this.updateComplete.then(() => { this.updateComplete.then(() => {
const input = (this.shadowRoot?.querySelector("paper-input") const input = (
?.inputElement as any)?.inputElement; this.shadowRoot?.querySelector("paper-input")?.inputElement as any
)?.inputElement;
if (input) { if (input) {
input.style.textOverflow = "ellipsis"; input.style.textOverflow = "ellipsis";
} }

View File

@ -1,6 +1,7 @@
import { Formfield } from "@material/mwc-formfield"; import { Formfield } from "@material/mwc-formfield";
import { css, CSSResultGroup } from "lit"; import { css, CSSResultGroup } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
@customElement("ha-formfield") @customElement("ha-formfield")
// @ts-expect-error // @ts-expect-error
export class HaFormfield extends Formfield { export class HaFormfield extends Formfield {

View File

@ -96,7 +96,7 @@ class HaHLSPlayer extends LitElement {
const useExoPlayerPromise = this._getUseExoPlayer(); const useExoPlayerPromise = this._getUseExoPlayer();
const masterPlaylistPromise = fetch(this.url); const masterPlaylistPromise = fetch(this.url);
const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min.js")) const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min"))
.default; .default;
let hlsSupported = Hls.isSupported(); let hlsSupported = Hls.isSupported();
@ -117,7 +117,8 @@ class HaHLSPlayer extends LitElement {
// Parse playlist assuming it is a master playlist. Match group 1 is whether hevc, match group 2 is regular playlist url // Parse playlist assuming it is a master playlist. Match group 1 is whether hevc, match group 2 is regular playlist url
// See https://tools.ietf.org/html/rfc8216 for HLS spec details // See https://tools.ietf.org/html/rfc8216 for HLS spec details
const playlistRegexp = /#EXT-X-STREAM-INF:.*?(?:CODECS=".*?(hev1|hvc1)?\..*?".*?)?(?:\n|\r\n)(.+)/g; const playlistRegexp =
/#EXT-X-STREAM-INF:.*?(?:CODECS=".*?(hev1|hvc1)?\..*?".*?)?(?:\n|\r\n)(.+)/g;
const match = playlistRegexp.exec(masterPlaylist); const match = playlistRegexp.exec(masterPlaylist);
const matchTwice = playlistRegexp.exec(masterPlaylist); const matchTwice = playlistRegexp.exec(masterPlaylist);

View File

@ -1,6 +1,5 @@
import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-icon-button/mwc-icon-button";
import { mdiImagePlus } from "@mdi/js"; import { mdiImagePlus } from "@mdi/js";
import "@polymer/iron-input/iron-input";
import "@polymer/paper-input/paper-input-container"; import "@polymer/paper-input/paper-input-container";
import { html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";

View File

@ -1,5 +1,6 @@
import { Radio } from "@material/mwc-radio"; import { Radio } from "@material/mwc-radio";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
@customElement("ha-radio") @customElement("ha-radio")
export class HaRadio extends Radio { export class HaRadio extends Radio {
public firstUpdated() { public firstUpdated() {

View File

@ -153,9 +153,8 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
</h3> </h3>
<ul> <ul>
${this._related.entity.map((entityId) => { ${this._related.entity.map((entityId) => {
const entity: HassEntity | undefined = this.hass.states[ const entity: HassEntity | undefined =
entityId this.hass.states[entityId];
];
if (!entity) { if (!entity) {
return ""; return "";
} }
@ -203,9 +202,8 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
<h3>${this.hass.localize("ui.components.related-items.scene")}:</h3> <h3>${this.hass.localize("ui.components.related-items.scene")}:</h3>
<ul> <ul>
${this._related.scene.map((sceneId) => { ${this._related.scene.map((sceneId) => {
const scene: SceneEntity | undefined = this.hass.states[ const scene: SceneEntity | undefined =
sceneId this.hass.states[sceneId];
];
if (!scene) { if (!scene) {
return ""; return "";
} }
@ -231,9 +229,8 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
</h3> </h3>
<ul> <ul>
${this._related.automation.map((automationId) => { ${this._related.automation.map((automationId) => {
const automation: HassEntity | undefined = this.hass.states[ const automation: HassEntity | undefined =
automationId this.hass.states[automationId];
];
if (!automation) { if (!automation) {
return ""; return "";
} }
@ -260,9 +257,8 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
</h3> </h3>
<ul> <ul>
${this._related.script.map((scriptId) => { ${this._related.script.map((scriptId) => {
const script: HassEntity | undefined = this.hass.states[ const script: HassEntity | undefined =
scriptId this.hass.states[scriptId];
];
if (!script) { if (!script) {
return ""; return "";
} }

View File

@ -1,4 +1,6 @@
import { mdiCheck } from "@mdi/js";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
@ -6,16 +8,35 @@ import { LocalizeFunc } from "../common/translations/localize";
import { domainToName } from "../data/integration"; import { domainToName } from "../data/integration";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-combo-box"; import "./ha-combo-box";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
const rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> = ( const rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> = (
item item
) => html`<style> ) => html`<style>
paper-item { paper-item {
margin: -10px 0;
padding: 0; padding: 0;
margin: -10px;
margin-left: 0px;
}
#content {
display: flex;
align-items: center;
}
:host([selected]) paper-item {
margin-left: 10px;
}
ha-svg-icon {
padding-left: 2px;
margin-right: -2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none;
}
:host([selected]) paper-icon-item {
margin-left: 0;
} }
</style> </style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-item> <paper-item>
<paper-item-body two-line> <paper-item-body two-line>
${item.name} ${item.name}

View File

@ -2,6 +2,7 @@ import { Switch } from "@material/mwc-switch";
import { css, CSSResultGroup } from "lit"; import { css, CSSResultGroup } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { forwardHaptic } from "../data/haptics"; import { forwardHaptic } from "../data/haptics";
@customElement("ha-switch") @customElement("ha-switch")
// @ts-expect-error // @ts-expect-error
export class HaSwitch extends Switch { export class HaSwitch extends Switch {

View File

@ -180,9 +180,8 @@ export class HaLocationsEditor extends LitElement {
const locationMarkers = {}; const locationMarkers = {};
const circles = {}; const circles = {};
const defaultZoneRadiusColor = getComputedStyle(this).getPropertyValue( const defaultZoneRadiusColor =
"--accent-color" getComputedStyle(this).getPropertyValue("--accent-color");
);
this.locations.forEach((location: MarkerLocation) => { this.locations.forEach((location: MarkerLocation) => {
let icon: DivIcon | undefined; let icon: DivIcon | undefined;

View File

@ -132,9 +132,8 @@ export class HaMediaPlayerBrowse extends LitElement {
return html``; return html``;
} }
const currentItem = this._mediaPlayerItems[ const currentItem =
this._mediaPlayerItems.length - 1 this._mediaPlayerItems[this._mediaPlayerItems.length - 1];
];
const previousItem: MediaPlayerItem | undefined = const previousItem: MediaPlayerItem | undefined =
this._mediaPlayerItems.length > 1 this._mediaPlayerItems.length > 1

View File

@ -111,14 +111,8 @@ export class HaTracePathDetails extends LitElement {
parts.push( parts.push(
data.map((trace, idx) => { data.map((trace, idx) => {
const { const { path, timestamp, result, error, changed_variables, ...rest } =
path, trace as any;
timestamp,
result,
error,
changed_variables,
...rest
} = trace as any;
return html` return html`
${curPath === this.selected.path ${curPath === this.selected.path

View File

@ -143,8 +143,9 @@ class HatScriptGraph extends LitElement {
graphStart = false graphStart = false
) { ) {
const trace = this.trace.trace[path] as ChooseActionTraceStep[] | undefined; const trace = this.trace.trace[path] as ChooseActionTraceStep[] | undefined;
const trace_path = trace?.[0].result const trace_path =
? trace[0].result.choice === "default" trace !== undefined
? trace[0].result === undefined || trace[0].result.choice === "default"
? [Array.isArray(config.choose) ? config.choose.length : 0] ? [Array.isArray(config.choose) ? config.choose.length : 0]
: [trace[0].result.choice] : [trace[0].result.choice]
: []; : [];
@ -177,9 +178,8 @@ class HatScriptGraph extends LitElement {
trace !== undefined && trace[0].result?.choice === i; trace !== undefined && trace[0].result?.choice === i;
this.renderedNodes[branch_path] = { config, path: branch_path }; this.renderedNodes[branch_path] = { config, path: branch_path };
if (track_this) { if (track_this) {
this.trackedNodes[branch_path] = this.renderedNodes[ this.trackedNodes[branch_path] =
branch_path this.renderedNodes[branch_path];
];
} }
return html` return html`
<hat-graph> <hat-graph>
@ -204,7 +204,9 @@ class HatScriptGraph extends LitElement {
<hat-graph-spacer <hat-graph-spacer
class=${classMap({ class=${classMap({
track: track:
trace !== undefined && trace[0].result?.choice === "default", trace !== undefined &&
(trace[0].result === undefined ||
trace[0].result.choice === "default"),
})} })}
></hat-graph-spacer> ></hat-graph-spacer>
${ensureArray(config.default)?.map((action, i) => ${ensureArray(config.default)?.map((action, i) =>
@ -486,9 +488,9 @@ class HatScriptGraph extends LitElement {
: ""} : ""}
${"condition" in this.trace.config ${"condition" in this.trace.config
? html`<hat-graph id="condition"> ? html`<hat-graph id="condition">
${ensureArray( ${ensureArray(this.trace.config.condition)?.map(
this.trace.config.condition (condition, i) => this.render_condition(condition!, i)
)?.map((condition, i) => this.render_condition(condition!, i))} )}
</hat-graph>` </hat-graph>`
: ""} : ""}
${"action" in this.trace.config ${"action" in this.trace.config
@ -532,12 +534,12 @@ class HatScriptGraph extends LitElement {
} }
} }
protected update(changedProps: PropertyValues<this>) { public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
if (changedProps.has("trace")) { if (changedProps.has("trace")) {
this.renderedNodes = {}; this.renderedNodes = {};
this.trackedNodes = {}; this.trackedNodes = {};
} }
super.update(changedProps);
} }
protected updated(changedProps: PropertyValues<this>) { protected updated(changedProps: PropertyValues<this>) {

View File

@ -121,9 +121,8 @@ class LogbookRenderer {
return; return;
} }
const previousEntryDate = this.pendingItems[ const previousEntryDate =
this.pendingItems.length - 1 this.pendingItems[this.pendingItems.length - 1][0];
][0];
// If logbook entry is too long after the last one, // If logbook entry is too long after the last one,
// add a time passed label // add a time passed label

View File

@ -10,6 +10,7 @@ export const callAlarmAction = (
| "arm_away" | "arm_away"
| "arm_home" | "arm_home"
| "arm_night" | "arm_night"
| "arm_vacation"
| "arm_custom_bypass" | "arm_custom_bypass"
| "disarm", | "disarm",
code?: string code?: string

View File

@ -6,7 +6,8 @@ export const subscribeBootstrapIntegrations = (
hass: HomeAssistant, hass: HomeAssistant,
callback: (message: BootstrapIntegrationsTimings) => void callback: (message: BootstrapIntegrationsTimings) => void
) => { ) => {
const unsubProm = hass.connection.subscribeMessage<BootstrapIntegrationsTimings>( const unsubProm =
hass.connection.subscribeMessage<BootstrapIntegrationsTimings>(
(message) => callback(message), (message) => callback(message),
{ {
type: "subscribe_bootstrap_integrations", type: "subscribe_bootstrap_integrations",

View File

@ -1,3 +1,4 @@
import { Connection } from "home-assistant-js-websocket";
import { HaFormSchema } from "../components/ha-form/ha-form"; import { HaFormSchema } from "../components/ha-form/ha-form";
import { ConfigEntry } from "./config_entries"; import { ConfigEntry } from "./config_entries";
@ -74,3 +75,12 @@ export type DataEntryFlowStep =
| DataEntryFlowStepCreateEntry | DataEntryFlowStepCreateEntry
| DataEntryFlowStepAbort | DataEntryFlowStepAbort
| DataEntryFlowStepProgress; | DataEntryFlowStepProgress;
export const subscribeDataEntryFlowProgressed = (
conn: Connection,
callback: (ev: DataEntryFlowProgressedEvent) => void
) =>
conn.subscribeEvents<DataEntryFlowProgressedEvent>(
callback,
"data_entry_flow_progressed"
);

View File

@ -9,7 +9,8 @@ export interface DiscoveryInformation {
version: string; version: string;
} }
export const fetchDiscoveryInformation = async (): Promise<DiscoveryInformation> => { export const fetchDiscoveryInformation =
async (): Promise<DiscoveryInformation> => {
const response = await fetch("/api/discovery_info", { method: "GET" }); const response = await fetch("/api/discovery_info", { method: "GET" });
return response.json(); return response.json();
}; };

131
src/data/energy.ts Normal file
View File

@ -0,0 +1,131 @@
import { HomeAssistant } from "../types";
export const emptyFlowFromGridSourceEnergyPreference =
(): FlowFromGridSourceEnergyPreference => ({
stat_energy_from: "",
stat_cost: null,
entity_energy_from: null,
entity_energy_price: null,
number_energy_price: null,
});
export const emptyFlowToGridSourceEnergyPreference =
(): FlowToGridSourceEnergyPreference => ({
stat_energy_to: "",
stat_compensation: null,
entity_energy_to: null,
entity_energy_price: null,
number_energy_price: null,
});
export const emptyGridSourceEnergyPreference =
(): GridSourceTypeEnergyPreference => ({
type: "grid",
flow_from: [],
flow_to: [],
cost_adjustment_day: 0,
});
export const emptySolarEnergyPreference =
(): SolarSourceTypeEnergyPreference => ({
type: "solar",
stat_energy_from: "",
config_entry_solar_forecast: null,
});
export interface DeviceConsumptionEnergyPreference {
// This is an ever increasing value
stat_consumption: string;
}
export interface FlowFromGridSourceEnergyPreference {
// kWh meter
stat_energy_from: string;
// $ meter
stat_cost: string | null;
// Can be used to generate costs if stat_cost omitted
entity_energy_from: string | null;
entity_energy_price: string | null;
number_energy_price: number | null;
}
export interface FlowToGridSourceEnergyPreference {
// kWh meter
stat_energy_to: string;
// $ meter
stat_compensation: string | null;
// Can be used to generate costs if stat_cost omitted
entity_energy_to: string | null;
entity_energy_price: string | null;
number_energy_price: number | null;
}
export interface GridSourceTypeEnergyPreference {
type: "grid";
flow_from: FlowFromGridSourceEnergyPreference[];
flow_to: FlowToGridSourceEnergyPreference[];
cost_adjustment_day: number;
}
export interface SolarSourceTypeEnergyPreference {
type: "solar";
stat_energy_from: string;
config_entry_solar_forecast: string[] | null;
}
type EnergySource =
| SolarSourceTypeEnergyPreference
| GridSourceTypeEnergyPreference;
export interface EnergyPreferences {
currency: string;
energy_sources: EnergySource[];
device_consumption: DeviceConsumptionEnergyPreference[];
}
export interface EnergyInfo {
cost_sensors: Record<string, string>;
}
export const getEnergyInfo = (hass: HomeAssistant) =>
hass.callWS<EnergyInfo>({
type: "energy/info",
});
export const getEnergyPreferences = (hass: HomeAssistant) =>
hass.callWS<EnergyPreferences>({
type: "energy/get_prefs",
});
export const saveEnergyPreferences = (
hass: HomeAssistant,
prefs: Partial<EnergyPreferences>
) =>
hass.callWS<EnergyPreferences>({
type: "energy/save_prefs",
...prefs,
});
interface EnergySourceByType {
grid?: GridSourceTypeEnergyPreference[];
solar?: SolarSourceTypeEnergyPreference[];
}
export const energySourcesByType = (prefs: EnergyPreferences) => {
const types: EnergySourceByType = {};
for (const source of prefs.energy_sources) {
if (source.type in types) {
types[source.type]!.push(source as any);
} else {
types[source.type] = [source as any];
}
}
return types;
};

View File

@ -0,0 +1,10 @@
import { HomeAssistant } from "../types";
export interface ForecastSolarForecast {
wh_hours: Record<string, number>;
}
export const getForecastSolarForecasts = (hass: HomeAssistant) =>
hass.callWS<Record<string, ForecastSolarForecast>>({
type: "forecast_solar/forecasts",
});

View File

@ -53,11 +53,33 @@ export interface HistoryResult {
timeline: TimelineEntity[]; timeline: TimelineEntity[];
} }
export type StatisticType = "sum" | "min" | "max" | "mean";
export interface Statistics {
[statisticId: string]: StatisticValue[];
}
export interface StatisticValue {
statistic_id: string;
start: string;
last_reset: string | null;
max: number | null;
mean: number | null;
min: number | null;
sum: number | null;
state: number | null;
}
export interface StatisticsMetaData {
unit_of_measurement: string;
statistic_id: string;
}
export const fetchRecent = ( export const fetchRecent = (
hass, hass: HomeAssistant,
entityId, entityId: string,
startTime, startTime: Date,
endTime, endTime: Date,
skipInitialState = false, skipInitialState = false,
significantChangesOnly?: boolean, significantChangesOnly?: boolean,
minimalResponse = true minimalResponse = true
@ -87,7 +109,7 @@ export const fetchDate = (
hass: HomeAssistant, hass: HomeAssistant,
startTime: Date, startTime: Date,
endTime: Date, endTime: Date,
entityId entityId?: string
): Promise<HassEntity[][]> => ): Promise<HassEntity[][]> =>
hass.callApi( hass.callApi(
"GET", "GET",
@ -252,3 +274,74 @@ export const computeHistory = (
return { line: unitStates, timeline: timelineDevices }; return { line: unitStates, timeline: timelineDevices };
}; };
// Statistics
export const getStatisticIds = (
hass: HomeAssistant,
statistic_type?: "mean" | "sum"
) =>
hass.callWS<StatisticsMetaData[]>({
type: "history/list_statistic_ids",
statistic_type,
});
export const fetchStatistics = (
hass: HomeAssistant,
startTime: Date,
endTime?: Date,
statistic_ids?: string[]
) =>
hass.callWS<Statistics>({
type: "history/statistics_during_period",
start_time: startTime.toISOString(),
end_time: endTime?.toISOString(),
statistic_ids,
});
export const calculateStatisticSumGrowth = (
values: StatisticValue[]
): number | null => {
if (values.length === 0) {
return null;
}
if (values.length === 1) {
return values[0].sum;
}
const endSum = values[values.length - 1].sum;
if (endSum === null) {
return null;
}
const startSum = values[0].sum;
if (startSum === null) {
return endSum;
}
return endSum - startSum;
};
export const calculateStatisticsSumGrowth = (
data: Statistics,
stats: string[]
): number | null => {
let totalGrowth = 0;
for (const stat of stats) {
if (!(stat in data)) {
return null;
}
const statGrowth = calculateStatisticSumGrowth(data[stat]);
if (statGrowth === null) {
return null;
}
totalGrowth += statGrowth;
}
return totalGrowth;
};
export const statisticsHaveType = (
stats: StatisticValue[],
type: StatisticType
) => stats.some((stat) => stat[type] !== null);

View File

@ -10,7 +10,7 @@ import {
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
import { computeRTL } from "../../common/util/compute_rtl"; import { computeRTL } from "../../common/util/compute_rtl";
import "../../components/ha-circular-progress"; import "../../components/ha-circular-progress";
import "../../components/ha-dialog"; import "../../components/ha-dialog";
@ -22,10 +22,10 @@ import {
subscribeAreaRegistry, subscribeAreaRegistry,
} from "../../data/area_registry"; } from "../../data/area_registry";
import { fetchConfigFlowInProgress } from "../../data/config_flow"; import { fetchConfigFlowInProgress } from "../../data/config_flow";
import type { import {
DataEntryFlowProgress, DataEntryFlowProgress,
DataEntryFlowProgressedEvent,
DataEntryFlowStep, DataEntryFlowStep,
subscribeDataEntryFlowProgressed,
} from "../../data/data_entry_flow"; } from "../../data/data_entry_flow";
import { import {
DeviceRegistryEntry, DeviceRegistryEntry,
@ -34,7 +34,10 @@ import {
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { showAlertDialog } from "../generic/show-dialog-box"; import { showAlertDialog } from "../generic/show-dialog-box";
import { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow"; import {
DataEntryFlowDialogParams,
LoadingReason,
} from "./show-dialog-data-entry-flow";
import "./step-flow-abort"; import "./step-flow-abort";
import "./step-flow-create-entry"; import "./step-flow-create-entry";
import "./step-flow-external"; import "./step-flow-external";
@ -46,13 +49,19 @@ import "./step-flow-progress";
let instance = 0; let instance = 0;
interface FlowUpdateEvent {
step?: DataEntryFlowStep;
stepPromise?: Promise<DataEntryFlowStep>;
}
declare global { declare global {
// for fire event // for fire event
interface HASSDomEvents { interface HASSDomEvents {
"flow-update": { "flow-update": FlowUpdateEvent;
step?: DataEntryFlowStep; }
stepPromise?: Promise<DataEntryFlowStep>; // for add event listener
}; interface HTMLElementEventMap {
"flow-update": HASSDomEvent<FlowUpdateEvent>;
} }
} }
@ -62,7 +71,7 @@ class DataEntryFlowDialog extends LitElement {
@state() private _params?: DataEntryFlowDialogParams; @state() private _params?: DataEntryFlowDialogParams;
@state() private _loading = true; @state() private _loading?: LoadingReason;
private _instance = instance; private _instance = instance;
@ -86,6 +95,8 @@ class DataEntryFlowDialog extends LitElement {
private _unsubDevices?: UnsubscribeFunc; private _unsubDevices?: UnsubscribeFunc;
private _unsubDataEntryFlowProgressed?: Promise<UnsubscribeFunc>;
public async showDialog(params: DataEntryFlowDialogParams): Promise<void> { public async showDialog(params: DataEntryFlowDialogParams): Promise<void> {
this._params = params; this._params = params;
this._instance = instance++; this._instance = instance++;
@ -96,7 +107,7 @@ class DataEntryFlowDialog extends LitElement {
} }
if (params.continueFlowId) { if (params.continueFlowId) {
this._loading = true; this._loading = "loading_flow";
const curInstance = this._instance; const curInstance = this._instance;
let step: DataEntryFlowStep; let step: DataEntryFlowStep;
try { try {
@ -124,7 +135,7 @@ class DataEntryFlowDialog extends LitElement {
} }
this._processStep(step); this._processStep(step);
this._loading = false; this._loading = undefined;
return; return;
} }
@ -136,14 +147,13 @@ class DataEntryFlowDialog extends LitElement {
// We only load the handlers once // We only load the handlers once
if (this._handlers === undefined) { if (this._handlers === undefined) {
this._loading = true; this._loading = "loading_handlers";
try { try {
this._handlers = await params.flowConfig.getFlowHandlers(this.hass); this._handlers = await params.flowConfig.getFlowHandlers(this.hass);
} finally { } finally {
this._loading = false; this._loading = undefined;
} }
} }
await this.updateComplete;
} }
public closeDialog() { public closeDialog() {
@ -159,9 +169,11 @@ class DataEntryFlowDialog extends LitElement {
this._params.flowConfig.deleteFlow(this.hass, this._step.flow_id); this._params.flowConfig.deleteFlow(this.hass, this._step.flow_id);
} }
if (this._step !== null && this._params.dialogClosedCallback) { if (this._step && this._params.dialogClosedCallback) {
this._params.dialogClosedCallback({ this._params.dialogClosedCallback({
flowFinished, flowFinished,
entryId:
"result" in this._step ? this._step.result?.entry_id : undefined,
}); });
} }
@ -178,6 +190,12 @@ class DataEntryFlowDialog extends LitElement {
this._unsubDevices(); this._unsubDevices();
this._unsubDevices = undefined; this._unsubDevices = undefined;
} }
if (this._unsubDataEntryFlowProgressed) {
this._unsubDataEntryFlowProgressed.then((unsub) => {
unsub();
});
this._unsubDataEntryFlowProgressed = undefined;
}
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
@ -201,9 +219,11 @@ class DataEntryFlowDialog extends LitElement {
this._handler === undefined) this._handler === undefined)
? html` ? html`
<step-flow-loading <step-flow-loading
.label=${this.hass.localize( .flowConfig=${this._params.flowConfig}
"ui.panel.config.integrations.config_flow.loading_first_time" .hass=${this.hass}
)} .loadingReason=${this._loading || "loading_handlers"}
.handler=${this._handler}
.step=${this._step}
></step-flow-loading> ></step-flow-loading>
` `
: this._step === undefined : this._step === undefined
@ -269,7 +289,13 @@ class DataEntryFlowDialog extends LitElement {
` `
: this._devices === undefined || this._areas === undefined : this._devices === undefined || this._areas === undefined
? // When it's a create entry result, we will fetch device & area registry ? // When it's a create entry result, we will fetch device & area registry
html` <step-flow-loading></step-flow-loading> ` html`
<step-flow-loading
.flowConfig=${this._params.flowConfig}
.hass=${this.hass}
loadingReason="loading_devices_areas"
></step-flow-loading>
`
: html` : html`
<step-flow-create-entry <step-flow-create-entry
.flowConfig=${this._params.flowConfig} .flowConfig=${this._params.flowConfig}
@ -287,31 +313,22 @@ class DataEntryFlowDialog extends LitElement {
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this.hass.connection.subscribeEvents<DataEntryFlowProgressedEvent>(
async (ev) => {
if (ev.data.flow_id !== this._step?.flow_id) {
return;
}
const step = await this._params!.flowConfig.fetchFlow(
this.hass,
this._step?.flow_id
);
this._processStep(step);
},
"data_entry_flow_progressed"
);
this.addEventListener("flow-update", (ev) => { this.addEventListener("flow-update", (ev) => {
const { step, stepPromise } = (ev as any).detail; const { step, stepPromise } = ev.detail;
this._processStep(step || stepPromise); this._processStep(step || stepPromise);
}); });
} }
protected updated(changedProps: PropertyValues) { public willUpdate(changedProps: PropertyValues) {
if ( super.willUpdate(changedProps);
changedProps.has("_step") && if (!changedProps.has("_step") || !this._step) {
this._step && return;
this._step.type === "create_entry" }
) { if (["external", "progress"].includes(this._step.type)) {
// external and progress step will send update event from the backend, so we should subscribe to them
this._subscribeDataEntryFlowProgressed();
}
if (this._step.type === "create_entry") {
if (this._step.result && this._params!.flowConfig.loadDevicesAndAreas) { if (this._step.result && this._params!.flowConfig.loadDevicesAndAreas) {
this._fetchDevices(this._step.result.entry_id); this._fetchDevices(this._step.result.entry_id);
this._fetchAreas(); this._fetchAreas();
@ -340,13 +357,16 @@ class DataEntryFlowDialog extends LitElement {
} }
private async _checkFlowsInProgress(handler: string) { private async _checkFlowsInProgress(handler: string) {
this._loading = true; this._loading = "loading_handlers";
this._handler = handler;
const flowsInProgress = ( const flowsInProgress = (
await fetchConfigFlowInProgress(this.hass.connection) await fetchConfigFlowInProgress(this.hass.connection)
).filter((flow) => flow.handler === handler); ).filter((flow) => flow.handler === handler);
if (!flowsInProgress.length) { if (!flowsInProgress.length) {
// No flows in progress, create a new flow
this._loading = "loading_flow";
let step: DataEntryFlowStep; let step: DataEntryFlowStep;
try { try {
step = await this._params!.flowConfig.createFlow(this.hass, handler); step = await this._params!.flowConfig.createFlow(this.hass, handler);
@ -362,14 +382,15 @@ class DataEntryFlowDialog extends LitElement {
), ),
}); });
return; return;
} finally {
this._handler = undefined;
} }
this._processStep(step); this._processStep(step);
} else { } else {
this._step = null; this._step = null;
this._handler = handler;
this._flowsInProgress = flowsInProgress; this._flowsInProgress = flowsInProgress;
} }
this._loading = false; this._loading = undefined;
} }
private _handlerPicked(ev) { private _handlerPicked(ev) {
@ -380,11 +401,11 @@ class DataEntryFlowDialog extends LitElement {
step: DataEntryFlowStep | undefined | Promise<DataEntryFlowStep> step: DataEntryFlowStep | undefined | Promise<DataEntryFlowStep>
): Promise<void> { ): Promise<void> {
if (step instanceof Promise) { if (step instanceof Promise) {
this._loading = true; this._loading = "loading_step";
try { try {
this._step = await step; this._step = await step;
} finally { } finally {
this._loading = false; this._loading = undefined;
} }
return; return;
} }
@ -398,6 +419,23 @@ class DataEntryFlowDialog extends LitElement {
this._step = step; this._step = step;
} }
private _subscribeDataEntryFlowProgressed() {
if (this._unsubDataEntryFlowProgressed) {
return;
}
this._unsubDataEntryFlowProgressed = subscribeDataEntryFlowProgressed(
this.hass.connection,
async (ev) => {
if (ev.data.flow_id !== this._step?.flow_id) {
return;
}
this._processStep(
this._params!.flowConfig.fetchFlow(this.hass, this._step?.flow_id)
);
}
);
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyleDialog, haStyleDialog,

View File

@ -39,6 +39,8 @@ export const showConfigFlowDialog = (
const [step] = await Promise.all([ const [step] = await Promise.all([
createConfigFlow(hass, handler), createConfigFlow(hass, handler),
hass.loadBackendTranslation("config", handler), hass.loadBackendTranslation("config", handler),
// Used as fallback if no header defined for step
hass.loadBackendTranslation("title", handler),
]); ]);
return step; return step;
}, },
@ -178,4 +180,22 @@ export const showConfigFlowDialog = (
` `
: ""; : "";
}, },
renderLoadingDescription(hass, reason, handler, step) {
if (!["loading_flow", "loading_step"].includes(reason)) {
return "";
}
const domain = step?.handler || handler;
return hass.localize(
`ui.panel.config.integrations.config_flow.loading.${reason}`,
{
integration: domain
? domainToName(hass.localize, domain)
: // when we are continuing a config flow, we only know the ID and not the domain
hass.localize(
"ui.panel.config.integrations.config_flow.loading.fallback_title"
),
}
);
},
}); });

View File

@ -79,12 +79,28 @@ export interface FlowConfig {
hass: HomeAssistant, hass: HomeAssistant,
step: DataEntryFlowStepProgress step: DataEntryFlowStepProgress
): TemplateResult | ""; ): TemplateResult | "";
renderLoadingDescription(
hass: HomeAssistant,
loadingReason: LoadingReason,
handler?: string,
step?: DataEntryFlowStep | null
): string;
} }
export type LoadingReason =
| "loading_handlers"
| "loading_flow"
| "loading_step"
| "loading_devices_areas";
export interface DataEntryFlowDialogParams { export interface DataEntryFlowDialogParams {
startFlowHandler?: string; startFlowHandler?: string;
continueFlowId?: string; continueFlowId?: string;
dialogClosedCallback?: (params: { flowFinished: boolean }) => void; dialogClosedCallback?: (params: {
flowFinished: boolean;
entryId?: string;
}) => void;
flowConfig: FlowConfig; flowConfig: FlowConfig;
showAdvanced?: boolean; showAdvanced?: boolean;
} }

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