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

View File

@ -10,26 +10,21 @@ on:
- dev
- master
env:
NODE_VERSION: 14
NODE_OPTIONS: --max_old_space_size=4096
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v2
- name: Setting up Node.js
uses: actions/setup-node@v1
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v2
with:
node-version: 12.x
- name: Get yarn cache path
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-
node-version: ${{ env.NODE_VERSION }}
cache: yarn
- name: Install dependencies
run: yarn install
env:
@ -42,51 +37,35 @@ jobs:
run: yarn run lint:types
- name: Run prettier
run: yarn run lint:prettier
- name: Check for duplicate dependencies
run: yarn dedupe --check
test:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v2
- name: Setting up Node.js
uses: actions/setup-node@v1
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v2
with:
node-version: 12.x
- name: Get yarn cache path
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-
node-version: ${{ env.NODE_VERSION }}
cache: yarn
- name: Install dependencies
run: yarn install
env:
CI: true
- name: Run Mocha
run: npm run mocha
run: yarn run mocha
build:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- name: Check out files from GitHub
uses: actions/checkout@v2
- name: Setting up Node.js
uses: actions/setup-node@v1
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v2
with:
node-version: 12.x
- name: Get yarn cache path
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-
node-version: ${{ env.NODE_VERSION }}
cache: yarn
- name: Install dependencies
run: yarn install
env:
@ -101,20 +80,11 @@ jobs:
steps:
- name: Check out files from GitHub
uses: actions/checkout@v2
- name: Setting up Node.js
uses: actions/setup-node@v1
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v2
with:
node-version: 12.x
- name: Get yarn cache path
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-
node-version: ${{ env.NODE_VERSION }}
cache: yarn
- name: Install dependencies
run: yarn install
env:

View File

@ -4,26 +4,22 @@ on:
push:
branches:
- dev
env:
NODE_VERSION: 14
NODE_OPTIONS: --max_old_space_size=4096
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v2
- name: Setting up Node.js
uses: actions/setup-node@v1
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v2
with:
node-version: 12.x
- name: Get yarn cache path
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-
node-version: ${{ env.NODE_VERSION }}
cache: yarn
- name: Install dependencies
run: yarn install
env:

View File

@ -7,7 +7,8 @@ on:
env:
PYTHON_VERSION: 3.8
NODE_VERSION: 12.1
NODE_VERSION: 14
NODE_OPTIONS: --max_old_space_size=4096
jobs:
release:
@ -29,7 +30,15 @@ jobs:
uses: actions/setup-node@v2
with:
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
run: |
python3 -m pip install twine

View File

@ -1,8 +1,6 @@
name: Translations
on:
schedule:
- cron: "30 0 * * *"
push:
branches:
- dev
@ -10,7 +8,7 @@ on:
- src/translations/en.json
env:
NODE_VERSION: 12
NODE_VERSION: 14
jobs:
upload:
@ -20,46 +18,8 @@ jobs:
- 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: Upload Translations
run: |
export LOKALISE_TOKEN="${{ secrets.LOKALISE_TOKEN }}"
./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
# yarn
.yarn
yarn-error.log
.yarn/*
!.yarn/patches
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*
node_modules/*
yarn-error.log
npm-debug.log
# 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"),
// Loads stuff from a CDN
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
latestBuild &&
// wrapped in require.resolve so it blows up if file no longer exists
@ -56,12 +57,23 @@ module.exports.babelOptions = ({ latestBuild }) => ({
"@babel/preset-env",
{
useBuiltIns: "entry",
corejs: "3.6",
corejs: "3.15",
bugfixes: true,
},
],
"@babel/preset-typescript",
].filter(Boolean),
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})
!latestBuild && [
"@babel/plugin-proposal-object-rest-spread",
@ -74,8 +86,14 @@ module.exports.babelOptions = ({ latestBuild }) => ({
"@babel/plugin-proposal-nullish-coalescing-operator",
["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],
["@babel/plugin-proposal-private-methods", { loose: true }],
["@babel/plugin-proposal-private-property-in-object", { loose: true }],
["@babel/plugin-proposal-class-properties", { loose: true }],
].filter(Boolean),
exclude: [
// \\ for Windows, / for Mac OS and Linux
/node_modules[\\/]core-js/,
/node_modules[\\/]webpack[\\/]buildin/,
],
});
const outputPath = (outputRoot, latestBuild) =>

View File

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

View File

@ -2,7 +2,6 @@
const gulp = require("gulp");
const path = require("path");
const cpx = require("cpx");
const fs = require("fs-extra");
const paths = require("../paths");
@ -62,9 +61,12 @@ function copyLoaderJS(staticDir) {
function copyFonts(staticDir) {
const staticPath = genStaticPath(staticDir);
// Local fonts
cpx.copySync(
npmPath("roboto-fontface/fonts/roboto/*.woff2"),
staticPath("fonts/roboto")
fs.copySync(
npmPath("roboto-fontface/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
? bothBuilds(createAppConfig, { isProdBuild: false })
: createAppConfig({ isProdBuild: false, latestBuild: true })
).watch({ ignored: /build-translations/, poll: isWsl }, doneHandler());
).watch(
{
ignored: /build-translations/,
poll: isWsl,
},
doneHandler()
);
gulp.watch(
path.join(paths.translations_src, "en.json"),
gulp.series("build-translations", "copy-translations-app")

View File

@ -49,12 +49,16 @@ const createWebpackConfig = ({
test: /\.m?js$|\.ts$/,
use: {
loader: "babel-loader",
options: bundle.babelOptions({ latestBuild }),
options: {
...bundle.babelOptions({ latestBuild }),
cacheDirectory: !isProdBuild,
cacheCompression: false,
},
},
},
{
test: /\.css$/,
use: "raw-loader",
type: "asset/source",
},
],
},
@ -66,6 +70,8 @@ const createWebpackConfig = ({
terserOptions: bundle.terserOptions(latestBuild),
}),
],
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
},
plugins: [
new WebpackManifestPlugin({
@ -112,16 +118,6 @@ const createWebpackConfig = ({
new RegExp(bundle.emptyPackages({ latestBuild }).join("|")),
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(),
].filter(Boolean),
resolve: {
@ -134,15 +130,13 @@ const createWebpackConfig = ({
},
output: {
filename: ({ chunk }) => {
if (!isProdBuild || dontHash.has(chunk.name)) {
if (!isProdBuild || isStatsBuild || dontHash.has(chunk.name)) {
return `${chunk.name}.js`;
}
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
},
chunkFilename:
isProdBuild && !isStatsBuild
? "chunk.[chunkhash].js"
: "[name].chunk.js",
isProdBuild && !isStatsBuild ? "[chunkhash:8].js" : "[id].chunk.js",
path: outputPath,
publicPath,
// To silence warning in worker plugin

View File

@ -139,7 +139,7 @@
Your authentication credentials or Home Assistant url are never sent
to the Cloud. You can validate this behavior in
<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"
>the source code</a
>.

View File

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

View File

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

View File

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

View File

@ -980,8 +980,7 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
icon: "mdi:account-off",
custom_ui_state_card: "state-card-custom-ui",
templates: {
icon:
"if (state === 'on') return 'mdi:account'; else if (state === 'off') return 'mdi:account-off';\n",
icon: "if (state === 'on') return 'mdi:account'; else if (state === 'off') return 'mdi:account-off';\n",
icon_color:
"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",
custom_ui_state_card: "state-card-custom-ui",
templates: {
icon:
"if (state === 'on') return 'mdi:account-group'; else if (state === 'off') return 'mdi:account-multiple-minus';\n",
icon: "if (state === 'on') return 'mdi:account-group'; else if (state === 'off') return 'mdi:account-multiple-minus';\n",
icon_color:
"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;
@state() private _switching?: boolean;
@state() private _switching = false;
private _hidden = localStorage.hide_demo_card;
@ -27,12 +27,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
return this._hidden ? 0 : 2;
}
public setConfig(
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
config: LovelaceCardConfig
// eslint-disable-next-line @typescript-eslint/no-empty-function
) {}
public setConfig(_config: LovelaceCardConfig) {}
protected render(): TemplateResult {
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/roboto";
import "../../src/resources/safari-14-attachshadow-patch";

View File

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

View File

@ -10,10 +10,9 @@ export const mockLovelace = (
localizePromise: Promise<LocalizeFunc>
) => {
hass.mockWS("lovelace/config", () =>
Promise.all([
selectedDemoConfig,
localizePromise,
]).then(([config, localize]) => config.lovelace(localize))
Promise.all([selectedDemoConfig, localizePromise]).then(
([config, localize]) => config.lovelace(localize)
)
);
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/trace/hat-script-graph";
import "../../../src/components/trace/hat-trace-timeline";
import { customElement, property, state } from "lit/decorators";
import { provideHass } from "../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../src/types";
import { DemoTrace } from "../data/traces/types";
import { basicTrace } from "../data/traces/basic_trace";
import { motionLightTrace } from "../data/traces/motion-light-trace";
import { customElement, property, state } from "lit/decorators";
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-switch";
import { classMap } from "lit/directives/class-map";
import { customElement, property, state } from "lit/decorators";
import { IntegrationManifest } from "../../../src/data/integration";
import { provideHass } from "../../../src/fake_data/provide_hass";
@ -15,8 +17,6 @@ import type {
} from "../../../src/panels/config/integrations/ha-config-integrations";
import { DeviceRegistryEntry } from "../../../src/data/device_registry";
import { EntityRegistryEntry } from "../../../src/data/entity_registry";
import { classMap } from "lit/directives/class-map";
import { customElement, property, state } from "lit/decorators";
const createConfigEntry = (
title: string,

View File

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

View File

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

View File

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

View File

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

View File

@ -41,7 +41,8 @@ const IP_VERSIONS = ["ipv4", "ipv6"];
@customElement("dialog-hassio-network")
export class DialogHassioNetwork
extends LitElement
implements HassDialog<HassioNetworkDialogParams> {
implements HassDialog<HassioNetworkDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@ -107,7 +108,7 @@ export class DialogHassioNetwork
</mwc-icon-button>
</ha-header-bar>
${this._interfaces.length > 1
? html` <mwc-tab-bar
? html`<mwc-tab-bar
.activeIndex=${this._curTabIndex}
@MDCTabBar:activated=${this._handleTabActivated}
>${this._interfaces.map(
@ -492,7 +493,7 @@ export class DialogHassioNetwork
}
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"
| "wep"
| "wpa-psk";

View File

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

View File

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

View File

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

View File

@ -30,7 +30,8 @@ import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot";
@customElement("dialog-hassio-snapshot")
class HassioSnapshotDialog
extends LitElement
implements HassDialog<HassioSnapshotDialogParams> {
implements HassDialog<HassioSnapshotDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _error?: string;
@ -297,8 +298,7 @@ class HassioSnapshotDialog
if (window.location.href.includes("ui.nabu.casa")) {
const confirm = await showConfirmationDialog(this, {
title: "Potential slow download",
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?",
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?",
confirmText: "continue",
dismissText: "cancel",
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,9 +2,9 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20210707.0",
version="20210726.0",
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_email="hello@home-assistant.io",
license="Apache-2.0",

View File

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

View File

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

View File

@ -82,14 +82,18 @@ class Storage {
const storage = new Storage();
export const LocalStorage = (
export const LocalStorage =
(
storageKey?: string,
property?: boolean,
propertyOptions?: PropertyDeclaration
): any => (clsElement: ClassElement) => {
): any =>
(clsElement: ClassElement) => {
const key = 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);
@ -145,4 +149,4 @@ export const LocalStorage = (
}
},
};
};
};

View File

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

View File

@ -43,7 +43,17 @@ export const domainIcon = (
: "hass:air-humidifier";
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":
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 { domainIcon } from "./domain_icon";
export const stateIcon = (state: HassEntity) => {
export const stateIcon = (state?: HassEntity) => {
if (!state) {
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") &&
(this.noUnderline || changedProps.get("noUnderline") !== undefined)
) {
(this._input.inputElement!.parentElement!.shadowRoot!.querySelector(
(
this._input.inputElement!.parentElement!.shadowRoot!.querySelector(
"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.
// 4. Disallow dates only consisting of a year.
// 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);

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,6 @@ import { customElement, property } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { HistoryResult } from "../../data/history";
import type { HomeAssistant } from "../../types";
import "../ha-circular-progress";
import "./state-history-chart-line";
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) {
super.draw(ctx);
const options = this.options as TextBaroptions;
const { x, y, base, width, text } = (this as BarElement<
TextBarProps,
TextBaroptions
>).getProps(["x", "y", "base", "width", "text"]);
const { x, y, base, width, text } = (
this as BarElement<TextBarProps, TextBaroptions>
).getProps(["x", "y", "base", "width", "text"]);
if (!text) {
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 {
css,
@ -10,10 +10,10 @@ import {
} from "lit";
import {
customElement,
property,
state,
query,
eventOptions,
property,
query,
state,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
@ -360,9 +360,8 @@ export class HaDataTable extends LitElement {
.rowId=${row[this.id]}
@click=${this._handleRowClick}
class="mdc-data-table__row ${classMap({
"mdc-data-table__row--selected": this._checkedRows.includes(
String(row[this.id])
),
"mdc-data-table__row--selected":
this._checkedRows.includes(String(row[this.id])),
clickable: this.clickable,
})}"
aria-selected=${ifDefined(
@ -406,17 +405,15 @@ export class HaDataTable extends LitElement {
"mdc-data-table__cell--icon": Boolean(
column.type === "icon"
),
"mdc-data-table__cell--icon-button": Boolean(
column.type === "icon-button"
),
"mdc-data-table__cell--icon-button":
Boolean(column.type === "icon-button"),
grows: Boolean(column.grows),
forceLTR: Boolean(column.forceLTR),
})}"
style=${column.width
? styleMap({
[column.grows
? "minWidth"
: "width"]: column.width,
[column.grows ? "minWidth" : "width"]:
column.width,
maxWidth: column.maxWidth
? column.maxWidth
: "",

View File

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

View File

@ -11,6 +11,8 @@ import {
} from "lit";
import { customElement, property, state, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { mdiCheck } from "@mdi/js";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { compare } from "../../common/string/compare";
@ -33,7 +35,6 @@ import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
interface Device {
name: string;
@ -47,10 +48,27 @@ export type HaDevicePickerDeviceFilterFunc = (
const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<style>
paper-item {
margin: -10px 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>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-item>
<paper-item-body two-line>
${item.name}

View File

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

View File

@ -1,5 +1,5 @@
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-item/paper-item";
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>
paper-item {
margin: -5px -10px;
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>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-item>${formatAttributeName(item)}</paper-item>`;
@customElement("ha-entity-attribute-picker")

View File

@ -1,5 +1,5 @@
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-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
@ -28,10 +28,25 @@ export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
const rowRenderer: ComboBoxLitRenderer<HassEntity> = (item) => html`<style>
paper-icon-item {
margin: -10px;
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-badge>
<paper-item-body two-line="">
@ -42,6 +57,8 @@ const rowRenderer: ComboBoxLitRenderer<HassEntity> = (item) => html`<style>
@customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled?: boolean;
@ -49,8 +66,6 @@ export class HaEntityPicker extends LitElement {
@property({ type: Boolean, attribute: "allow-custom-entity" })
public allowCustomEntity;
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@ -79,6 +94,14 @@ export class HaEntityPicker extends LitElement {
@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() public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Boolean }) public hideClearIcon = false;
@ -110,7 +133,8 @@ export class HaEntityPicker extends LitElement {
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
entityFilter: this["entityFilter"],
includeDeviceClasses: this["includeDeviceClasses"]
includeDeviceClasses: this["includeDeviceClasses"],
includeUnitOfMeasurement: this["includeUnitOfMeasurement"]
) => {
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) {
states = states.filter(
(stateObj) =>
@ -184,7 +220,7 @@ export class HaEntityPicker extends LitElement {
return !(!changedProps.has("_opened") && this._opened);
}
protected updated(changedProps: PropertyValues) {
public willUpdate(changedProps: PropertyValues) {
if (!this._initedStates || (changedProps.has("_opened") && this._opened)) {
this._states = this._getStates(
this._opened,
@ -192,23 +228,24 @@ export class HaEntityPicker extends LitElement {
this.includeDomains,
this.excludeDomains,
this.entityFilter,
this.includeDeviceClasses
this.includeDeviceClasses,
this.includeUnitOfMeasurement
);
if (this._initedStates) {
(this.comboBox as any).filteredItems = this._states;
}
this._initedStates = true;
}
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<vaadin-combo-box-light
item-value-path="entity_id"
item-label-path="entity_id"
.value=${this._value}
.allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._states}
${comboBoxRenderer(rowRenderer)}
@opened-changed=${this._openedChanged}
@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 { 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 { fireEvent } from "../common/dom/fire_event";
import { compare } from "../common/string/compare";
@ -9,14 +11,33 @@ import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types";
import { HaComboBox } from "./ha-combo-box";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) => html`<style>
paper-item {
margin: -10px 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>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-item>
<paper-item-body two-line>
${item.name}

View File

@ -1,5 +1,5 @@
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-item/paper-item";
import "@polymer/paper-item/paper-item-body";
@ -48,13 +48,27 @@ const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (
item
) => html`<style>
paper-item {
margin: -10px 0;
padding: 0;
margin: -10px;
margin-left: 0;
}
paper-item.add-new {
font-weight: 500;
#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>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-item class=${classMap({ "add-new": item.area_id === "add_new" })}>
<paper-item-body two-line>${item.name}</paper-item-body>
</paper-item>`;

View File

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

View File

@ -1,12 +1,15 @@
import { Dialog } from "@material/mwc-dialog";
import { mdiClose } from "@mdi/js";
import { css, CSSResultGroup, html } from "lit";
import { css, CSSResultGroup, html, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import { computeRTLDirection } from "../common/util/compute_rtl";
import type { HomeAssistant } from "../types";
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>
<mwc-icon-button
aria-label=${hass.localize("ui.dialogs.generic.close")}

View File

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

View File

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

View File

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

View File

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

View File

@ -96,7 +96,7 @@ class HaHLSPlayer extends LitElement {
const useExoPlayerPromise = this._getUseExoPlayer();
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;
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
// 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 matchTwice = playlistRegexp.exec(masterPlaylist);

View File

@ -33,10 +33,10 @@ class HaLabelBadge extends LitElement {
>
<slot>
${this.icon && !this.value && !this.image
? html` <ha-icon .icon=${this.icon}></ha-icon> `
? html`<ha-icon .icon=${this.icon}></ha-icon>`
: ""}
${this.value && !this.image
? html` <span>${this.value}</span> `
? html`<span>${this.value}</span>`
: ""}
</slot>
</div>

View File

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

View File

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

View File

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

View File

@ -1,4 +1,6 @@
import { mdiCheck } from "@mdi/js";
import { html, LitElement } from "lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
@ -6,16 +8,35 @@ import { LocalizeFunc } from "../common/translations/localize";
import { domainToName } from "../data/integration";
import { HomeAssistant } from "../types";
import "./ha-combo-box";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
const rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> = (
item
) => html`<style>
paper-item {
margin: -10px 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>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-item>
<paper-item-body two-line>
${item.name}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,8 @@ export const subscribeBootstrapIntegrations = (
hass: HomeAssistant,
callback: (message: BootstrapIntegrationsTimings) => void
) => {
const unsubProm = hass.connection.subscribeMessage<BootstrapIntegrationsTimings>(
const unsubProm =
hass.connection.subscribeMessage<BootstrapIntegrationsTimings>(
(message) => callback(message),
{
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 { ConfigEntry } from "./config_entries";
@ -74,3 +75,12 @@ export type DataEntryFlowStep =
| DataEntryFlowStepCreateEntry
| DataEntryFlowStepAbort
| 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;
}
export const fetchDiscoveryInformation = async (): Promise<DiscoveryInformation> => {
export const fetchDiscoveryInformation =
async (): Promise<DiscoveryInformation> => {
const response = await fetch("/api/discovery_info", { method: "GET" });
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[];
}
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 = (
hass,
entityId,
startTime,
endTime,
hass: HomeAssistant,
entityId: string,
startTime: Date,
endTime: Date,
skipInitialState = false,
significantChangesOnly?: boolean,
minimalResponse = true
@ -87,7 +109,7 @@ export const fetchDate = (
hass: HomeAssistant,
startTime: Date,
endTime: Date,
entityId
entityId?: string
): Promise<HassEntity[][]> =>
hass.callApi(
"GET",
@ -252,3 +274,74 @@ export const computeHistory = (
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,
} from "lit";
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 "../../components/ha-circular-progress";
import "../../components/ha-dialog";
@ -22,10 +22,10 @@ import {
subscribeAreaRegistry,
} from "../../data/area_registry";
import { fetchConfigFlowInProgress } from "../../data/config_flow";
import type {
import {
DataEntryFlowProgress,
DataEntryFlowProgressedEvent,
DataEntryFlowStep,
subscribeDataEntryFlowProgressed,
} from "../../data/data_entry_flow";
import {
DeviceRegistryEntry,
@ -34,7 +34,10 @@ import {
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
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-create-entry";
import "./step-flow-external";
@ -46,13 +49,19 @@ import "./step-flow-progress";
let instance = 0;
interface FlowUpdateEvent {
step?: DataEntryFlowStep;
stepPromise?: Promise<DataEntryFlowStep>;
}
declare global {
// for fire event
interface HASSDomEvents {
"flow-update": {
step?: DataEntryFlowStep;
stepPromise?: Promise<DataEntryFlowStep>;
};
"flow-update": FlowUpdateEvent;
}
// for add event listener
interface HTMLElementEventMap {
"flow-update": HASSDomEvent<FlowUpdateEvent>;
}
}
@ -62,7 +71,7 @@ class DataEntryFlowDialog extends LitElement {
@state() private _params?: DataEntryFlowDialogParams;
@state() private _loading = true;
@state() private _loading?: LoadingReason;
private _instance = instance;
@ -86,6 +95,8 @@ class DataEntryFlowDialog extends LitElement {
private _unsubDevices?: UnsubscribeFunc;
private _unsubDataEntryFlowProgressed?: Promise<UnsubscribeFunc>;
public async showDialog(params: DataEntryFlowDialogParams): Promise<void> {
this._params = params;
this._instance = instance++;
@ -96,7 +107,7 @@ class DataEntryFlowDialog extends LitElement {
}
if (params.continueFlowId) {
this._loading = true;
this._loading = "loading_flow";
const curInstance = this._instance;
let step: DataEntryFlowStep;
try {
@ -124,7 +135,7 @@ class DataEntryFlowDialog extends LitElement {
}
this._processStep(step);
this._loading = false;
this._loading = undefined;
return;
}
@ -136,14 +147,13 @@ class DataEntryFlowDialog extends LitElement {
// We only load the handlers once
if (this._handlers === undefined) {
this._loading = true;
this._loading = "loading_handlers";
try {
this._handlers = await params.flowConfig.getFlowHandlers(this.hass);
} finally {
this._loading = false;
this._loading = undefined;
}
}
await this.updateComplete;
}
public closeDialog() {
@ -159,9 +169,11 @@ class DataEntryFlowDialog extends LitElement {
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({
flowFinished,
entryId:
"result" in this._step ? this._step.result?.entry_id : undefined,
});
}
@ -178,6 +190,12 @@ class DataEntryFlowDialog extends LitElement {
this._unsubDevices();
this._unsubDevices = undefined;
}
if (this._unsubDataEntryFlowProgressed) {
this._unsubDataEntryFlowProgressed.then((unsub) => {
unsub();
});
this._unsubDataEntryFlowProgressed = undefined;
}
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@ -201,9 +219,11 @@ class DataEntryFlowDialog extends LitElement {
this._handler === undefined)
? html`
<step-flow-loading
.label=${this.hass.localize(
"ui.panel.config.integrations.config_flow.loading_first_time"
)}
.flowConfig=${this._params.flowConfig}
.hass=${this.hass}
.loadingReason=${this._loading || "loading_handlers"}
.handler=${this._handler}
.step=${this._step}
></step-flow-loading>
`
: this._step === undefined
@ -269,7 +289,13 @@ class DataEntryFlowDialog extends LitElement {
`
: this._devices === undefined || this._areas === undefined
? // 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`
<step-flow-create-entry
.flowConfig=${this._params.flowConfig}
@ -287,31 +313,22 @@ class DataEntryFlowDialog extends LitElement {
protected firstUpdated(changedProps: PropertyValues) {
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) => {
const { step, stepPromise } = (ev as any).detail;
const { step, stepPromise } = ev.detail;
this._processStep(step || stepPromise);
});
}
protected updated(changedProps: PropertyValues) {
if (
changedProps.has("_step") &&
this._step &&
this._step.type === "create_entry"
) {
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!changedProps.has("_step") || !this._step) {
return;
}
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) {
this._fetchDevices(this._step.result.entry_id);
this._fetchAreas();
@ -340,13 +357,16 @@ class DataEntryFlowDialog extends LitElement {
}
private async _checkFlowsInProgress(handler: string) {
this._loading = true;
this._loading = "loading_handlers";
this._handler = handler;
const flowsInProgress = (
await fetchConfigFlowInProgress(this.hass.connection)
).filter((flow) => flow.handler === handler);
if (!flowsInProgress.length) {
// No flows in progress, create a new flow
this._loading = "loading_flow";
let step: DataEntryFlowStep;
try {
step = await this._params!.flowConfig.createFlow(this.hass, handler);
@ -362,14 +382,15 @@ class DataEntryFlowDialog extends LitElement {
),
});
return;
} finally {
this._handler = undefined;
}
this._processStep(step);
} else {
this._step = null;
this._handler = handler;
this._flowsInProgress = flowsInProgress;
}
this._loading = false;
this._loading = undefined;
}
private _handlerPicked(ev) {
@ -380,11 +401,11 @@ class DataEntryFlowDialog extends LitElement {
step: DataEntryFlowStep | undefined | Promise<DataEntryFlowStep>
): Promise<void> {
if (step instanceof Promise) {
this._loading = true;
this._loading = "loading_step";
try {
this._step = await step;
} finally {
this._loading = false;
this._loading = undefined;
}
return;
}
@ -398,6 +419,23 @@ class DataEntryFlowDialog extends LitElement {
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 {
return [
haStyleDialog,

View File

@ -39,6 +39,8 @@ export const showConfigFlowDialog = (
const [step] = await Promise.all([
createConfigFlow(hass, handler),
hass.loadBackendTranslation("config", handler),
// Used as fallback if no header defined for step
hass.loadBackendTranslation("title", handler),
]);
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"
),
}
);
},
});

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