Compare commits

..

8 Commits

Author SHA1 Message Date
Ludeeus
50047e2f10 Adjust gap 2021-07-30 10:21:27 +00:00
Ludeeus
2811541fba update 2021-06-02 21:22:34 +00:00
Ludeeus
57788cec44 style 2021-06-02 17:02:24 +00:00
Ludeeus
0aa314d9ae overlay 2021-06-02 16:57:41 +00:00
Ludeeus
f8d97735b8 update 2021-06-02 16:09:28 +00:00
Ludeeus
830136b874 icons 2021-06-02 15:42:31 +00:00
Ludeeus
dd01710784 addon 2021-06-02 15:31:05 +00:00
Ludeeus
f7d0736731 init 2021-06-02 15:10:18 +00:00
510 changed files with 24271 additions and 41484 deletions

View File

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

View File

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

View File

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

View File

@@ -6,9 +6,9 @@ on:
- published - published
env: env:
WHEELS_TAG: 3.8-alpine3.12
PYTHON_VERSION: 3.8 PYTHON_VERSION: 3.8
NODE_VERSION: 14 NODE_VERSION: 12.1
NODE_OPTIONS: --max_old_space_size=4096
jobs: jobs:
release: release:
@@ -30,15 +30,7 @@ jobs:
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn
- name: Install dependencies
run: yarn install
- name: Download Translations
run: ./script/translations_download
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build and release package - name: Build and release package
run: | run: |
python3 -m pip install twine python3 -m pip install twine
@@ -72,9 +64,6 @@ jobs:
strategy: strategy:
matrix: matrix:
arch: ["aarch64", "armhf", "armv7", "amd64", "i386"] arch: ["aarch64", "armhf", "armv7", "amd64", "i386"]
tag:
- "3.8-alpine3.12"
- "3.9-alpine3.13"
steps: steps:
- name: Download requirements.txt - name: Download requirements.txt
uses: actions/download-artifact@v2 uses: actions/download-artifact@v2
@@ -84,7 +73,7 @@ jobs:
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@master uses: home-assistant/wheels@master
with: with:
tag: ${{ matrix.tag }} tag: ${{ env.WHEELS_TAG }}
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
wheels-host: ${{ secrets.WHEELS_HOST }} wheels-host: ${{ secrets.WHEELS_HOST }}
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}

View File

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

10
.gitignore vendored
View File

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

2
.nvmrc
View File

@@ -1 +1 @@
14 12.1

File diff suppressed because one or more lines are too long

View File

@@ -1,29 +0,0 @@
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

@@ -1,34 +0,0 @@
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

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +0,0 @@
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

@@ -1,170 +0,0 @@
/* 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

@@ -5,6 +5,8 @@ const paths = require("./paths.js");
// Files from NPM Packages that should not be imported // Files from NPM Packages that should not be imported
module.exports.ignorePackages = ({ latestBuild }) => [ module.exports.ignorePackages = ({ latestBuild }) => [
// Bloats bundle and it's not used.
path.resolve(require.resolve("moment"), "../locale"),
// Part of yaml.js and only used for !!js functions that we don't use // Part of yaml.js and only used for !!js functions that we don't use
require.resolve("esprima"), require.resolve("esprima"),
]; ];
@@ -18,8 +20,7 @@ module.exports.emptyPackages = ({ latestBuild }) =>
require.resolve("@polymer/paper-styles/default-theme.js"), require.resolve("@polymer/paper-styles/default-theme.js"),
// Loads stuff from a CDN // Loads stuff from a CDN
require.resolve("@polymer/font-roboto/roboto.js"), require.resolve("@polymer/font-roboto/roboto.js"),
require.resolve("@vaadin/vaadin-material-styles/typography.js"), require.resolve("@vaadin/vaadin-material-styles/font-roboto.js"),
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
// Compatibility not needed for latest builds // Compatibility not needed for latest builds
latestBuild && latestBuild &&
// wrapped in require.resolve so it blows up if file no longer exists // wrapped in require.resolve so it blows up if file no longer exists
@@ -51,29 +52,17 @@ module.exports.terserOptions = (latestBuild) => ({
module.exports.babelOptions = ({ latestBuild }) => ({ module.exports.babelOptions = ({ latestBuild }) => ({
babelrc: false, babelrc: false,
compact: false,
presets: [ presets: [
!latestBuild && [ !latestBuild && [
"@babel/preset-env", "@babel/preset-env",
{ {
useBuiltIns: "entry", useBuiltIns: "entry",
corejs: "3.15", corejs: "3.6",
bugfixes: true,
}, },
], ],
"@babel/preset-typescript", "@babel/preset-typescript",
].filter(Boolean), ].filter(Boolean),
plugins: [ plugins: [
[
path.resolve(
paths.polymer_dir,
"build-scripts/babel-plugins/inline-constants-plugin.js"
),
{
modules: ["@mdi/js"],
ignoreModuleNotFound: true,
},
],
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2}) // Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
!latestBuild && [ !latestBuild && [
"@babel/plugin-proposal-object-rest-spread", "@babel/plugin-proposal-object-rest-spread",
@@ -86,16 +75,16 @@ module.exports.babelOptions = ({ latestBuild }) => ({
"@babel/plugin-proposal-nullish-coalescing-operator", "@babel/plugin-proposal-nullish-coalescing-operator",
["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }], ["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],
["@babel/plugin-proposal-private-methods", { loose: true }], ["@babel/plugin-proposal-private-methods", { loose: true }],
["@babel/plugin-proposal-private-property-in-object", { loose: true }],
["@babel/plugin-proposal-class-properties", { loose: true }], ["@babel/plugin-proposal-class-properties", { loose: true }],
].filter(Boolean), ].filter(Boolean),
exclude: [
// \\ for Windows, / for Mac OS and Linux
/node_modules[\\/]core-js/,
/node_modules[\\/]webpack[\\/]buildin/,
],
}); });
// Are already ES5, cause warnings when babelified.
module.exports.babelExclude = () => [
require.resolve("@mdi/js/mdi.js"),
require.resolve("hls.js"),
];
const outputPath = (outputRoot, latestBuild) => const outputPath = (outputRoot, latestBuild) =>
path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5"); path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5");

View File

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

View File

@@ -302,23 +302,15 @@ gulp.task("gen-index-hassio-prod", async () => {
function writeHassioEntrypoint(latestEntrypoint, es5Entrypoint) { function writeHassioEntrypoint(latestEntrypoint, es5Entrypoint) {
fs.mkdirSync(paths.hassio_output_root, { recursive: true }); fs.mkdirSync(paths.hassio_output_root, { recursive: true });
// Safari 12 and below does not have a compliant ES2015 implementation of template literals, so we ship ES5
fs.writeFileSync( fs.writeFileSync(
path.resolve(paths.hassio_output_root, "entrypoint.js"), path.resolve(paths.hassio_output_root, "entrypoint.js"),
` `
function loadES5() { try {
new Function("import('${latestEntrypoint}')")();
} catch (err) {
var el = document.createElement('script'); var el = document.createElement('script');
el.src = '${es5Entrypoint}'; el.src = '${es5Entrypoint}';
document.body.appendChild(el); document.body.appendChild(el);
}
if (/.*Version\\/(?:11|12)(?:\\.\\d+)*.*Safari\\//.test(navigator.userAgent)) {
loadES5();
} else {
try {
new Function("import('${latestEntrypoint}')")();
} catch (err) {
loadES5();
}
} }
`, `,
{ encoding: "utf-8" } { encoding: "utf-8" }

View File

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

View File

@@ -1,5 +1,4 @@
// Tasks to run webpack. // Tasks to run webpack.
const fs = require("fs");
const gulp = require("gulp"); const gulp = require("gulp");
const webpack = require("webpack"); const webpack = require("webpack");
const WebpackDevServer = require("webpack-dev-server"); const WebpackDevServer = require("webpack-dev-server");
@@ -19,13 +18,6 @@ const bothBuilds = (createConfigFunc, params) => [
createConfigFunc({ ...params, latestBuild: false }), createConfigFunc({ ...params, latestBuild: false }),
]; ];
const isWsl =
fs.existsSync("/proc/version") &&
fs
.readFileSync("/proc/version", "utf-8")
.toLocaleLowerCase()
.includes("microsoft");
/** /**
* @param {{ * @param {{
* compiler: import("webpack").Compiler, * compiler: import("webpack").Compiler,
@@ -86,15 +78,8 @@ const prodBuild = (conf) =>
gulp.task("webpack-watch-app", () => { gulp.task("webpack-watch-app", () => {
// This command will run forever because we don't close compiler // This command will run forever because we don't close compiler
webpack( webpack(createAppConfig({ isProdBuild: false, latestBuild: true })).watch(
process.env.ES5 { ignored: /build-translations/ },
? bothBuilds(createAppConfig, { isProdBuild: false })
: createAppConfig({ isProdBuild: false, latestBuild: true })
).watch(
{
ignored: /build-translations/,
poll: isWsl,
},
doneHandler() doneHandler()
); );
gulp.watch( gulp.watch(
@@ -152,7 +137,7 @@ gulp.task("webpack-watch-hassio", () => {
isProdBuild: false, isProdBuild: false,
latestBuild: true, latestBuild: true,
}) })
).watch({ ignored: /build-translations/, poll: isWsl }, doneHandler()); ).watch({ ignored: /build-translations/ }, doneHandler());
gulp.watch( gulp.watch(
path.join(paths.translations_src, "en.json"), path.join(paths.translations_src, "en.json"),

View File

@@ -57,6 +57,7 @@ const createRollupConfig = ({
babel({ babel({
...bundle.babelOptions({ latestBuild }), ...bundle.babelOptions({ latestBuild }),
extensions, extensions,
exclude: bundle.babelExclude(),
babelHelpers: isWDS ? "inline" : "bundled", babelHelpers: isWDS ? "inline" : "bundled",
}), }),
string({ string({

View File

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

View File

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

View File

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

View File

@@ -113,7 +113,8 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
on: "/assets/arsaboo/icons/light_bulb_on.png", on: "/assets/arsaboo/icons/light_bulb_on.png",
}, },
state_filter: { state_filter: {
on: "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)", off: "brightness(80%) saturate(0.8)",
}, },
style: { style: {
@@ -195,7 +196,8 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
on: "/assets/arsaboo/icons/light_bulb_on.png", on: "/assets/arsaboo/icons/light_bulb_on.png",
}, },
state_filter: { state_filter: {
on: "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)", off: "brightness(80%) saturate(0.8)",
}, },
style: { style: {
@@ -275,7 +277,8 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
on: "/assets/arsaboo/icons/light_bulb_on.png", on: "/assets/arsaboo/icons/light_bulb_on.png",
}, },
state_filter: { state_filter: {
on: "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)", off: "brightness(80%) saturate(0.8)",
}, },
style: { style: {
@@ -312,7 +315,8 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
on: "/assets/arsaboo/icons/light_bulb_on.png", on: "/assets/arsaboo/icons/light_bulb_on.png",
}, },
state_filter: { state_filter: {
on: "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)", off: "brightness(80%) saturate(0.8)",
}, },
style: { style: {

View File

@@ -1,6 +1,5 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import { Lovelace } from "../../../src/panels/lovelace/types"; import { Lovelace } from "../../../src/panels/lovelace/types";
import { energyEntities } from "../stubs/entities";
import { DemoConfig } from "./types"; import { DemoConfig } from "./types";
export const demoConfigs: Array<() => Promise<DemoConfig>> = [ export const demoConfigs: Array<() => Promise<DemoConfig>> = [
@@ -13,8 +12,9 @@ export const demoConfigs: Array<() => Promise<DemoConfig>> = [
// eslint-disable-next-line import/no-mutable-exports // eslint-disable-next-line import/no-mutable-exports
export let selectedDemoConfigIndex = 0; export let selectedDemoConfigIndex = 0;
// eslint-disable-next-line import/no-mutable-exports // eslint-disable-next-line import/no-mutable-exports
export let selectedDemoConfig: Promise<DemoConfig> = export let selectedDemoConfig: Promise<DemoConfig> = demoConfigs[
demoConfigs[selectedDemoConfigIndex](); selectedDemoConfigIndex
]();
export const setDemoConfig = async ( export const setDemoConfig = async (
hass: MockHomeAssistant, hass: MockHomeAssistant,
@@ -28,7 +28,6 @@ export const setDemoConfig = async (
selectedDemoConfig = confProm; selectedDemoConfig = confProm;
hass.addEntities(config.entities(hass.localize), true); hass.addEntities(config.entities(hass.localize), true);
hass.addEntities(energyEntities());
lovelace.saveConfig(config.lovelace(hass.localize)); lovelace.saveConfig(config.lovelace(hass.localize));
hass.mockTheme(config.theme()); hass.mockTheme(config.theme());
}; };

View File

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

View File

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

View File

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

View File

@@ -20,10 +20,6 @@ import { mockShoppingList } from "./stubs/shopping_list";
import { mockSystemLog } from "./stubs/system_log"; import { mockSystemLog } from "./stubs/system_log";
import { mockTemplate } from "./stubs/template"; import { mockTemplate } from "./stubs/template";
import { mockTranslations } from "./stubs/translations"; import { mockTranslations } from "./stubs/translations";
import { mockEnergy } from "./stubs/energy";
import { mockConfig } from "./stubs/config";
import { energyEntities } from "./stubs/entities";
import { mockForecastSolar } from "./stubs/forecast_solar";
class HaDemo extends HomeAssistantAppEl { class HaDemo extends HomeAssistantAppEl {
protected async _initializeHass() { protected async _initializeHass() {
@@ -51,13 +47,8 @@ class HaDemo extends HomeAssistantAppEl {
mockEvents(hass); mockEvents(hass);
mockMediaPlayer(hass); mockMediaPlayer(hass);
mockFrontend(hass); mockFrontend(hass);
mockEnergy(hass);
mockForecastSolar(hass);
mockConfig(hass);
mockPersistentNotification(hass); mockPersistentNotification(hass);
hass.addEntities(energyEntities());
// Once config is loaded AND localize, set entities and apply theme. // Once config is loaded AND localize, set entities and apply theme.
Promise.all([selectedDemoConfig, localizePromise]).then( Promise.all([selectedDemoConfig, localizePromise]).then(
([conf, localize]) => { ([conf, localize]) => {

View File

@@ -1,41 +0,0 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockConfig = (hass: MockHomeAssistant) => {
hass.mockAPI("config/config_entries/entry", () => [
{
entry_id: "co2signal",
domain: "co2signal",
title: "CO2 Signal",
source: "user",
state: "loaded",
supports_options: false,
supports_unload: true,
pref_disable_new_entities: false,
pref_disable_polling: false,
disabled_by: null,
reason: null,
},
]);
hass.mockWS("config/entity_registry/list", () => [
{
config_entry_id: "co2signal",
device_id: "co2signal",
area_id: null,
disabled_by: null,
entity_id: "sensor.co2_intensity",
name: null,
icon: null,
platform: "co2signal",
},
{
config_entry_id: "co2signal",
device_id: "co2signal",
area_id: null,
disabled_by: null,
entity_id: "sensor.grid_fossil_fuel_percentage",
name: null,
icon: null,
platform: "co2signal",
},
]);
};

View File

@@ -1,70 +0,0 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockEnergy = (hass: MockHomeAssistant) => {
hass.mockWS("energy/get_prefs", () => ({
energy_sources: [
{
type: "grid",
flow_from: [
{
stat_energy_from: "sensor.energy_consumption_tarif_1",
stat_cost: "sensor.energy_consumption_tarif_1_cost",
entity_energy_from: "sensor.energy_consumption_tarif_1",
entity_energy_price: null,
number_energy_price: null,
},
{
stat_energy_from: "sensor.energy_consumption_tarif_2",
stat_cost: "sensor.energy_consumption_tarif_2_cost",
entity_energy_from: "sensor.energy_consumption_tarif_2",
entity_energy_price: null,
number_energy_price: null,
},
],
flow_to: [
{
stat_energy_to: "sensor.energy_production_tarif_1",
stat_compensation: "sensor.energy_production_tarif_1_compensation",
entity_energy_to: "sensor.energy_production_tarif_1",
entity_energy_price: null,
number_energy_price: null,
},
{
stat_energy_to: "sensor.energy_production_tarif_2",
stat_compensation: "sensor.energy_production_tarif_2_compensation",
entity_energy_to: "sensor.energy_production_tarif_2",
entity_energy_price: null,
number_energy_price: null,
},
],
cost_adjustment_day: 0,
},
{
type: "solar",
stat_energy_from: "sensor.solar_production",
config_entry_solar_forecast: ["solar_forecast"],
},
],
device_consumption: [
{
stat_consumption: "sensor.energy_car",
},
{
stat_consumption: "sensor.energy_ac",
},
{
stat_consumption: "sensor.energy_washing_machine",
},
{
stat_consumption: "sensor.energy_dryer",
},
{
stat_consumption: "sensor.energy_heat_pump",
},
{
stat_consumption: "sensor.energy_boiler",
},
],
}));
hass.mockWS("energy/info", () => ({ cost_sensors: [] }));
};

View File

@@ -1,143 +0,0 @@
import { convertEntities } from "../../../src/fake_data/entity";
export const energyEntities = () =>
convertEntities({
"sensor.grid_fossil_fuel_percentage": {
entity_id: "sensor.grid_fossil_fuel_percentage",
state: "88.6",
attributes: {
unit_of_measurement: "%",
},
},
"sensor.solar_production": {
entity_id: "sensor.solar_production",
state: "88.6",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Solar",
unit_of_measurement: "kWh",
},
},
"sensor.energy_consumption_tarif_1": {
entity_id: "sensor.energy_consumption_tarif_1 ",
state: "88.6",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Grid consumption low tariff",
unit_of_measurement: "kWh",
},
},
"sensor.energy_consumption_tarif_2": {
entity_id: "sensor.energy_consumption_tarif_2",
state: "88.6",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Grid consumption high tariff",
unit_of_measurement: "kWh",
},
},
"sensor.energy_production_tarif_1": {
entity_id: "sensor.energy_production_tarif_1",
state: "88.6",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Returned to grid low tariff",
unit_of_measurement: "kWh",
},
},
"sensor.energy_production_tarif_2": {
entity_id: "sensor.energy_production_tarif_2",
state: "88.6",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Returned to grid high tariff",
unit_of_measurement: "kWh",
},
},
"sensor.energy_consumption_tarif_1_cost": {
entity_id: "sensor.energy_consumption_tarif_1_cost",
state: "2",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
unit_of_measurement: "EUR",
},
},
"sensor.energy_consumption_tarif_2_cost": {
entity_id: "sensor.energy_consumption_tarif_2_cost",
state: "2",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
unit_of_measurement: "EUR",
},
},
"sensor.energy_production_tarif_1_compensation": {
entity_id: "sensor.energy_production_tarif_1_compensation",
state: "2",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
unit_of_measurement: "EUR",
},
},
"sensor.energy_production_tarif_2_compensation": {
entity_id: "sensor.energy_production_tarif_2_compensation",
state: "2",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
unit_of_measurement: "EUR",
},
},
"sensor.energy_car": {
entity_id: "sensor.energy_car",
state: "4",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Electric car",
unit_of_measurement: "kWh",
},
},
"sensor.energy_ac": {
entity_id: "sensor.energy_ac",
state: "3",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Air conditioning",
unit_of_measurement: "kWh",
},
},
"sensor.energy_washing_machine": {
entity_id: "sensor.energy_washing_machine",
state: "6",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Washing machine",
unit_of_measurement: "kWh",
},
},
"sensor.energy_dryer": {
entity_id: "sensor.energy_dryer",
state: "5.5",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Dryer",
unit_of_measurement: "kWh",
},
},
"sensor.energy_heat_pump": {
entity_id: "sensor.energy_heat_pump",
state: "6",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Heat pump",
unit_of_measurement: "kWh",
},
},
"sensor.energy_boiler": {
entity_id: "sensor.energy_boiler",
state: "7",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Boiler",
unit_of_measurement: "kWh",
},
},
});

View File

@@ -1,55 +0,0 @@
import { format, startOfToday, startOfTomorrow } from "date-fns";
import { ForecastSolarForecast } from "../../../src/data/forecast_solar";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockForecastSolar = (hass: MockHomeAssistant) => {
const todayString = format(startOfToday(), "yyyy-MM-dd");
const tomorrowString = format(startOfTomorrow(), "yyyy-MM-dd");
hass.mockWS(
"forecast_solar/forecasts",
(): Record<string, ForecastSolarForecast> => ({
solar_forecast: {
wh_hours: {
[`${todayString}T06:00:00`]: 0,
[`${todayString}T06:23:00`]: 6,
[`${todayString}T06:45:00`]: 39,
[`${todayString}T07:00:00`]: 28,
[`${todayString}T08:00:00`]: 208,
[`${todayString}T09:00:00`]: 352,
[`${todayString}T10:00:00`]: 544,
[`${todayString}T11:00:00`]: 748,
[`${todayString}T12:00:00`]: 1259,
[`${todayString}T13:00:00`]: 1361,
[`${todayString}T14:00:00`]: 1373,
[`${todayString}T15:00:00`]: 1370,
[`${todayString}T16:00:00`]: 1186,
[`${todayString}T17:00:00`]: 937,
[`${todayString}T18:00:00`]: 652,
[`${todayString}T19:00:00`]: 370,
[`${todayString}T20:00:00`]: 155,
[`${todayString}T21:48:00`]: 24,
[`${todayString}T22:36:00`]: 0,
[`${tomorrowString}T06:01:00`]: 0,
[`${tomorrowString}T06:23:00`]: 9,
[`${tomorrowString}T06:45:00`]: 47,
[`${tomorrowString}T07:00:00`]: 48,
[`${tomorrowString}T08:00:00`]: 473,
[`${tomorrowString}T09:00:00`]: 827,
[`${tomorrowString}T10:00:00`]: 1153,
[`${tomorrowString}T11:00:00`]: 1413,
[`${tomorrowString}T12:00:00`]: 1590,
[`${tomorrowString}T13:00:00`]: 1652,
[`${tomorrowString}T14:00:00`]: 1612,
[`${tomorrowString}T15:00:00`]: 1438,
[`${tomorrowString}T16:00:00`]: 1149,
[`${tomorrowString}T17:00:00`]: 830,
[`${tomorrowString}T18:00:00`]: 542,
[`${tomorrowString}T19:00:00`]: 311,
[`${tomorrowString}T20:00:00`]: 140,
[`${tomorrowString}T21:47:00`]: 22,
[`${tomorrowString}T22:34:00`]: 0,
},
},
})
);
};

View File

@@ -1,6 +1,4 @@
import { addHours, differenceInHours } from "date-fns";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { StatisticValue } from "../../../src/data/history";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
interface HistoryQueryParams { interface HistoryQueryParams {
@@ -66,215 +64,17 @@ const generateHistory = (state, deltas) => {
const incrementalUnits = ["clients", "queries", "ads"]; const incrementalUnits = ["clients", "queries", "ads"];
const generateMeanStatistics = (
id: string,
start: Date,
end: Date,
initValue: number,
maxDiff: number
) => {
const statistics: StatisticValue[] = [];
let currentDate = new Date(start);
currentDate.setMinutes(0, 0, 0);
let lastVal = initValue;
const now = new Date();
while (end > currentDate && currentDate < now) {
const delta = Math.random() * maxDiff;
const mean = lastVal + delta;
statistics.push({
statistic_id: id,
start: currentDate.toISOString(),
mean,
min: mean,
max: mean,
last_reset: "1970-01-01T00:00:00+00:00",
state: mean,
sum: null,
});
lastVal = mean;
currentDate = addHours(currentDate, 1);
}
return statistics;
};
const generateSumStatistics = (
id: string,
start: Date,
end: Date,
initValue: number,
maxDiff: number
) => {
const statistics: StatisticValue[] = [];
let currentDate = new Date(start);
currentDate.setMinutes(0, 0, 0);
let sum = initValue;
const now = new Date();
while (end > currentDate && currentDate < now) {
const add = Math.random() * maxDiff;
sum += add;
statistics.push({
statistic_id: id,
start: currentDate.toISOString(),
mean: null,
min: null,
max: null,
last_reset: "1970-01-01T00:00:00+00:00",
state: initValue + sum,
sum,
});
currentDate = addHours(currentDate, 1);
}
return statistics;
};
const generateCurvedStatistics = (
id: string,
start: Date,
end: Date,
initValue: number,
maxDiff: number,
metered: boolean
) => {
const statistics: StatisticValue[] = [];
let currentDate = new Date(start);
currentDate.setMinutes(0, 0, 0);
let sum = initValue;
const hours = differenceInHours(end, start) - 1;
let i = 0;
let half = false;
const now = new Date();
while (end > currentDate && currentDate < now) {
const add = Math.random() * maxDiff;
sum += i * add;
statistics.push({
statistic_id: id,
start: currentDate.toISOString(),
mean: null,
min: null,
max: null,
last_reset: "1970-01-01T00:00:00+00:00",
state: initValue + sum,
sum: metered ? sum : null,
});
currentDate = addHours(currentDate, 1);
if (!half && i > hours / 2) {
half = true;
}
i += half ? -1 : 1;
}
return statistics;
};
const statisticsFunctions: Record<
string,
(id: string, start: Date, end: Date) => StatisticValue[]
> = {
"sensor.energy_consumption_tarif_1": (id: string, start: Date, end: Date) => {
const morningEnd = new Date(start.getTime() + 10 * 60 * 60 * 1000);
const morningLow = generateSumStatistics(id, start, morningEnd, 0, 0.7);
const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000);
const morningFinalVal = morningLow.length
? morningLow[morningLow.length - 1].sum!
: 0;
const empty = generateSumStatistics(
id,
morningEnd,
eveningStart,
morningFinalVal,
0
);
const eveningLow = generateSumStatistics(
id,
eveningStart,
end,
morningFinalVal,
0.7
);
return [...morningLow, ...empty, ...eveningLow];
},
"sensor.energy_consumption_tarif_2": (id: string, start: Date, end: Date) => {
const morningEnd = new Date(start.getTime() + 9 * 60 * 60 * 1000);
const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000);
const highTarif = generateSumStatistics(
id,
morningEnd,
eveningStart,
0,
0.3
);
const highTarifFinalVal = highTarif.length
? highTarif[highTarif.length - 1].sum!
: 0;
const morning = generateSumStatistics(id, start, morningEnd, 0, 0);
const evening = generateSumStatistics(
id,
eveningStart,
end,
highTarifFinalVal,
0
);
return [...morning, ...highTarif, ...evening];
},
"sensor.energy_production_tarif_1": (id, start, end) =>
generateSumStatistics(id, start, end, 0, 0),
"sensor.energy_production_tarif_1_compensation": (id, start, end) =>
generateSumStatistics(id, start, end, 0, 0),
"sensor.energy_production_tarif_2": (id, start, end) => {
const productionStart = new Date(start.getTime() + 9 * 60 * 60 * 1000);
const productionEnd = new Date(start.getTime() + 21 * 60 * 60 * 1000);
const production = generateCurvedStatistics(
id,
productionStart,
productionEnd,
0,
0.15,
true
);
const productionFinalVal = production.length
? production[production.length - 1].sum!
: 0;
const morning = generateSumStatistics(id, start, productionStart, 0, 0);
const evening = generateSumStatistics(
id,
productionEnd,
end,
productionFinalVal,
0
);
return [...morning, ...production, ...evening];
},
"sensor.solar_production": (id, start, end) => {
const productionStart = new Date(start.getTime() + 7 * 60 * 60 * 1000);
const productionEnd = new Date(start.getTime() + 23 * 60 * 60 * 1000);
const production = generateCurvedStatistics(
id,
productionStart,
productionEnd,
0,
0.3,
true
);
const productionFinalVal = production.length
? production[production.length - 1].sum!
: 0;
const morning = generateSumStatistics(id, start, productionStart, 0, 0);
const evening = generateSumStatistics(
id,
productionEnd,
end,
productionFinalVal,
0
);
return [...morning, ...production, ...evening];
},
"sensor.grid_fossil_fuel_percentage": (id, start, end) =>
generateMeanStatistics(id, start, end, 35, 1.3),
};
export const mockHistory = (mockHass: MockHomeAssistant) => { export const mockHistory = (mockHass: MockHomeAssistant) => {
mockHass.mockAPI( mockHass.mockAPI(
new RegExp("history/period/.+"), new RegExp("history/period/.+"),
(hass, _method, path, _parameters) => { (
hass,
// @ts-ignore
method,
path,
// @ts-ignore
parameters
) => {
const params = parseQuery<HistoryQueryParams>(path.split("?")[1]); const params = parseQuery<HistoryQueryParams>(path.split("?")[1]);
const entities = params.filter_entity_id.split(","); const entities = params.filter_entity_id.split(",");
@@ -295,7 +95,7 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
const numberState = Number(state.state); const numberState = Number(state.state);
if (isNaN(numberState)) { if (isNaN(numberState)) {
// eslint-disable-next-line no-console // eslint-disable-next-line
console.log( console.log(
"Ignoring state with unparsable state but with a unit", "Ignoring state with unparsable state but with a unit",
entityId, entityId,
@@ -340,39 +140,4 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
return results; return results;
} }
); );
mockHass.mockWS(
"history/statistics_during_period",
({ statistic_ids, start_time, end_time }, hass) => {
const start = new Date(start_time);
const end = new Date(end_time);
const statistics: Record<string, StatisticValue[]> = {};
statistic_ids.forEach((id: string) => {
if (id in statisticsFunctions) {
statistics[id] = statisticsFunctions[id](id, start, end);
} else {
const entityState = hass.states[id];
const state = entityState ? Number(entityState.state) : 1;
statistics[id] =
entityState && "last_reset" in entityState.attributes
? generateSumStatistics(
id,
start,
end,
state,
state * (state > 80 ? 0.01 : 0.05)
)
: generateMeanStatistics(
id,
start,
end,
state,
state * (state > 80 ? 0.05 : 0.1)
);
}
});
return statistics;
}
);
}; };

View File

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

View File

@@ -6,7 +6,7 @@ export const mockTemplate = (hass: MockHomeAssistant) => {
body: { message: "Template dev tool does not work in the demo." }, body: { message: "Template dev tool does not work in the demo." },
}) })
); );
hass.mockWS("render_template", (msg, _hass, onChange) => { hass.mockWS("render_template", (msg, onChange) => {
onChange!({ onChange!({
result: msg.template, result: msg.template,
listeners: { all: false, domains: [], entities: [], time: false }, listeners: { all: false, domains: [], entities: [], time: false },

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import "@material/mwc-button";
import { ActionDetail } from "@material/mwc-list"; import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js"; import { mdiDotsVertical } from "@mdi/js";
import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea";
import { DEFAULT_SCHEMA, Type } from "js-yaml"; import { DEFAULT_SCHEMA, Type } from "js-yaml";
import { import {
css, css,
@@ -133,7 +134,7 @@ class HassioAddonConfig extends LitElement {
></ha-form>` ></ha-form>`
: html` <ha-yaml-editor : html` <ha-yaml-editor
@value-changed=${this._configChanged} @value-changed=${this._configChanged}
.yamlSchema=${ADDON_YAML_SCHEMA} .schema=${ADDON_YAML_SCHEMA}
></ha-yaml-editor>`} ></ha-yaml-editor>`}
${this._error ? html` <div class="errors">${this._error}</div> ` : ""} ${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
${!this._yamlMode || ${!this._yamlMode ||
@@ -268,9 +269,6 @@ class HassioAddonConfig extends LitElement {
private async _saveTapped(ev: CustomEvent): Promise<void> { private async _saveTapped(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any; const button = ev.currentTarget as any;
const options: Record<string, unknown> = this._yamlMode
? this._editor?.value
: this._options;
const eventdata = { const eventdata = {
success: true, success: true,
response: undefined, response: undefined,
@@ -284,13 +282,13 @@ class HassioAddonConfig extends LitElement {
const validation = await validateHassioAddonOption( const validation = await validateHassioAddonOption(
this.hass, this.hass,
this.addon.slug, this.addon.slug,
options this._editor?.value
); );
if (!validation.valid) { if (!validation.valid) {
throw Error(validation.message); throw Error(validation.message);
} }
await setHassioAddonOption(this.hass, this.addon.slug, { await setHassioAddonOption(this.hass, this.addon.slug, {
options, options: this._yamlMode ? this._editor?.value : this._options,
}); });
this._configHasChanged = false; this._configHasChanged = false;
@@ -328,6 +326,10 @@ class HassioAddonConfig extends LitElement {
color: var(--error-color); color: var(--error-color);
margin-top: 16px; margin-top: 16px;
} }
iron-autogrow-textarea {
width: 100%;
font-family: var(--code-font-family, monospace);
}
.syntaxerror { .syntaxerror {
color: var(--error-color); color: var(--error-color);
} }

View File

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

View File

@@ -892,19 +892,10 @@ class HassioAddonInfo extends LitElement {
private async _openChangelog(): Promise<void> { private async _openChangelog(): Promise<void> {
try { try {
let content = await fetchHassioAddonChangelog(this.hass, this.addon.slug); const content = await fetchHassioAddonChangelog(
if ( this.hass,
content.includes(`# ${this.addon.version}`) && this.addon.slug
content.includes(`# ${this.addon.version_latest}`) );
) {
const newcontent = content.split(`# ${this.addon.version}`)[0];
if (newcontent.includes(`# ${this.addon.version_latest}`)) {
// Only change the content if the new version still exist
// if the changelog does not have the newests version on top
// this will not be true, and we don't modify the content
content = newcontent;
}
}
showHassioMarkdownDialog(this, { showHassioMarkdownDialog(this, {
title: this.supervisor.localize("addon.dashboard.changelog"), title: this.supervisor.localize("addon.dashboard.changelog"),
content, content,

View File

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

View File

@@ -29,6 +29,7 @@ class SupervisorFormfieldLabel extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host { :host {
cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
} }

View File

@@ -5,7 +5,6 @@ import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version"; import { atLeastVersion } from "../../../src/common/config/version";
import { formatDate } from "../../../src/common/datetime/format_date"; import { formatDate } from "../../../src/common/datetime/format_date";
import { formatDateTime } from "../../../src/common/datetime/format_date_time"; import { formatDateTime } from "../../../src/common/datetime/format_date_time";
import { LocalizeFunc } from "../../../src/common/translations/localize";
import "../../../src/components/ha-checkbox"; import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-formfield"; import "../../../src/components/ha-formfield";
import "../../../src/components/ha-radio"; import "../../../src/components/ha-radio";
@@ -45,9 +44,6 @@ const _computeFolders = (folders): CheckboxItem[] => {
if (folders.includes("share")) { if (folders.includes("share")) {
list.push({ slug: "share", name: "Share", checked: false }); list.push({ slug: "share", name: "Share", checked: false });
} }
if (folders.includes("media")) {
list.push({ slug: "media", name: "Media", checked: false });
}
if (folders.includes("addons/local")) { if (folders.includes("addons/local")) {
list.push({ slug: "addons/local", name: "Local add-ons", checked: false }); list.push({ slug: "addons/local", name: "Local add-ons", checked: false });
} }
@@ -68,8 +64,6 @@ const _computeAddons = (addons): AddonCheckboxItem[] =>
export class SupervisorSnapshotContent extends LitElement { export class SupervisorSnapshotContent extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public localize?: LocalizeFunc;
@property({ attribute: false }) public supervisor?: Supervisor; @property({ attribute: false }) public supervisor?: Supervisor;
@property({ attribute: false }) public snapshot?: HassioSnapshotDetail; @property({ attribute: false }) public snapshot?: HassioSnapshotDetail;
@@ -84,14 +78,10 @@ export class SupervisorSnapshotContent extends LitElement {
@property({ type: Boolean }) public snapshotHasPassword = false; @property({ type: Boolean }) public snapshotHasPassword = false;
@property({ type: Boolean }) public onboarding = false;
@property() public snapshotName = ""; @property() public snapshotName = "";
@property() public snapshotPassword = ""; @property() public snapshotPassword = "";
@property() public confirmSnapshotPassword = "";
public willUpdate(changedProps) { public willUpdate(changedProps) {
super.willUpdate(changedProps); super.willUpdate(changedProps);
if (!this.hasUpdated) { if (!this.hasUpdated) {
@@ -111,12 +101,8 @@ export class SupervisorSnapshotContent extends LitElement {
} }
} }
private _localize = (string: string) =>
this.supervisor?.localize(`snapshot.${string}`) ||
this.localize!(`ui.panel.page-onboarding.restore.${string}`);
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.onboarding && !this.supervisor) { if (!this.supervisor) {
return html``; return html``;
} }
const foldersSection = const foldersSection =
@@ -128,16 +114,14 @@ export class SupervisorSnapshotContent extends LitElement {
${this.snapshot ${this.snapshot
? html`<div class="details"> ? html`<div class="details">
${this.snapshot.type === "full" ${this.snapshot.type === "full"
? this._localize("full_snapshot") ? this.supervisor.localize("snapshot.full_snapshot")
: this._localize("partial_snapshot")} : this.supervisor.localize("snapshot.partial_snapshot")}
(${Math.ceil(this.snapshot.size * 10) / 10 + " MB"})<br /> (${Math.ceil(this.snapshot.size * 10) / 10 + " MB"})<br />
${this.hass ${formatDateTime(new Date(this.snapshot.date), this.hass.locale)}
? formatDateTime(new Date(this.snapshot.date), this.hass.locale)
: this.snapshot.date}
</div>` </div>`
: html`<paper-input : html`<paper-input
name="snapshotName" name="snapshotName"
.label=${this.supervisor?.localize("snapshot.name") || "Name"} .label=${this.supervisor.localize("snapshot.name")}
.value=${this.snapshotName} .value=${this.snapshotName}
@value-changed=${this._handleTextValueChanged} @value-changed=${this._handleTextValueChanged}
> >
@@ -145,11 +129,13 @@ export class SupervisorSnapshotContent extends LitElement {
${!this.snapshot || this.snapshot.type === "full" ${!this.snapshot || this.snapshot.type === "full"
? html`<div class="sub-header"> ? html`<div class="sub-header">
${!this.snapshot ${!this.snapshot
? this._localize("type") ? this.supervisor.localize("snapshot.type")
: this._localize("select_type")} : this.supervisor.localize("snapshot.select_type")}
</div> </div>
<div class="snapshot-types"> <div class="snapshot-types">
<ha-formfield .label=${this._localize("full_snapshot")}> <ha-formfield
.label=${this.supervisor.localize("snapshot.full_snapshot")}
>
<ha-radio <ha-radio
@change=${this._handleRadioValueChanged} @change=${this._handleRadioValueChanged}
value="full" value="full"
@@ -158,7 +144,9 @@ export class SupervisorSnapshotContent extends LitElement {
> >
</ha-radio> </ha-radio>
</ha-formfield> </ha-formfield>
<ha-formfield .label=${this._localize("partial_snapshot")}> <ha-formfield
.label=${this.supervisor!.localize("snapshot.partial_snapshot")}
>
<ha-radio <ha-radio
@change=${this._handleRadioValueChanged} @change=${this._handleRadioValueChanged}
value="partial" value="partial"
@@ -169,9 +157,9 @@ export class SupervisorSnapshotContent extends LitElement {
</ha-formfield> </ha-formfield>
</div>` </div>`
: ""} : ""}
${this.snapshotType === "partial" ${this.snapshot && this.snapshotType === "partial"
? html`<div class="partial-picker"> ? html`
${this.snapshot && this.snapshot.homeassistant ${this.snapshot.homeassistant
? html` ? html`
<ha-formfield <ha-formfield
.label=${html`<supervisor-formfield-label .label=${html`<supervisor-formfield-label
@@ -191,11 +179,15 @@ export class SupervisorSnapshotContent extends LitElement {
</ha-formfield> </ha-formfield>
` `
: ""} : ""}
`
: ""}
${this.snapshotType === "partial"
? html`
${foldersSection?.templates.length ${foldersSection?.templates.length
? html` ? html`
<ha-formfield <ha-formfield
.label=${html`<supervisor-formfield-label .label=${html`<supervisor-formfield-label
.label=${this._localize("folders")} .label=${this.supervisor.localize("snapshot.folders")}
.iconPath=${mdiFolder} .iconPath=${mdiFolder}
> >
</supervisor-formfield-label>`} </supervisor-formfield-label>`}
@@ -215,7 +207,7 @@ export class SupervisorSnapshotContent extends LitElement {
? html` ? html`
<ha-formfield <ha-formfield
.label=${html`<supervisor-formfield-label .label=${html`<supervisor-formfield-label
.label=${this._localize("addons")} .label=${this.supervisor.localize("snapshot.addons")}
.iconPath=${mdiPuzzle} .iconPath=${mdiPuzzle}
> >
</supervisor-formfield-label>`} </supervisor-formfield-label>`}
@@ -231,44 +223,29 @@ export class SupervisorSnapshotContent extends LitElement {
<div class="section-content">${addonsSection.templates}</div> <div class="section-content">${addonsSection.templates}</div>
` `
: ""} : ""}
</div> ` `
: ""}
${this.snapshotType === "partial" &&
(!this.snapshot || this.snapshotHasPassword)
? html`<hr />`
: ""} : ""}
${!this.snapshot ${!this.snapshot
? html`<ha-formfield ? html`<ha-formfield
class="password" .label=${this.supervisor.localize("snapshot.password_protection")}
.label=${this._localize("password_protection")}
> >
<ha-checkbox <ha-checkbox
.checked=${this.snapshotHasPassword} .checked=${this.snapshotHasPassword}
@change=${this._toggleHasPassword} @change=${this._toggleHasPassword}
> >
</ha-checkbox> </ha-checkbox
</ha-formfield>` ></ha-formfield>`
: ""} : ""}
${this.snapshotHasPassword ${this.snapshotHasPassword
? html` ? html`
<paper-input <paper-input
.label=${this._localize("password")} .label=${this.supervisor.localize("snapshot.password")}
type="password" type="password"
name="snapshotPassword" name="snapshotPassword"
.value=${this.snapshotPassword} .value=${this.snapshotPassword}
@value-changed=${this._handleTextValueChanged} @value-changed=${this._handleTextValueChanged}
> >
</paper-input> </paper-input>
${!this.snapshot
? html` <paper-input
.label=${this.supervisor?.localize("confirm_password")}
type="password"
name="confirmSnapshotPassword"
.value=${this.confirmSnapshotPassword}
@value-changed=${this._handleTextValueChanged}
>
</paper-input>`
: ""}
` `
: ""} : ""}
`; `;
@@ -276,24 +253,21 @@ export class SupervisorSnapshotContent extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
.partial-picker ha-formfield { ha-checkbox {
--mdc-checkbox-touch-target-size: 16px;
display: block; display: block;
margin: 4px 12px 8px 0;
} }
.partial-picker ha-checkbox { ha-formfield {
--mdc-checkbox-touch-target-size: 32px; display: contents;
}
.partial-picker {
display: block;
margin: 0px -6px;
} }
supervisor-formfield-label { supervisor-formfield-label {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
} }
hr { paper-input[type="password"] {
border-color: var(--divider-color); display: block;
border-bottom: none; margin: 4px 0 4px 16px;
margin: 16px 0;
} }
.details { .details {
color: var(--secondary-text-color); color: var(--secondary-text-color);
@@ -301,15 +275,13 @@ export class SupervisorSnapshotContent extends LitElement {
.section-content { .section-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-left: 30px; margin-left: 16px;
} }
ha-formfield.password { .security {
display: block; margin-top: 16px;
margin: 0 -14px -16px;
} }
.snapshot-types { .snapshot-types {
display: flex; display: flex;
margin-left: -13px;
} }
.sub-header { .sub-header {
margin-top: 8px; margin-top: 8px;
@@ -328,9 +300,6 @@ export class SupervisorSnapshotContent extends LitElement {
if (this.snapshotHasPassword) { if (this.snapshotHasPassword) {
data.password = this.snapshotPassword; data.password = this.snapshotPassword;
if (!this.snapshot) {
data.confirm_password = this.confirmSnapshotPassword;
}
} }
if (this.snapshotType === "full") { if (this.snapshotType === "full") {
@@ -362,7 +331,7 @@ export class SupervisorSnapshotContent extends LitElement {
const addons = const addons =
section === "addons" section === "addons"
? new Map( ? new Map(
this.supervisor?.addon.addons.map((item) => [item.slug, item]) this.supervisor!.addon.addons.map((item) => [item.slug, item])
) )
: undefined; : undefined;
let checkedItems = 0; let checkedItems = 0;
@@ -372,7 +341,6 @@ export class SupervisorSnapshotContent extends LitElement {
.label=${item.name} .label=${item.name}
.iconPath=${section === "addons" ? mdiPuzzle : mdiFolder} .iconPath=${section === "addons" ? mdiPuzzle : mdiFolder}
.imageUrl=${section === "addons" && .imageUrl=${section === "addons" &&
!this.onboarding &&
atLeastVersion(this.hass.config.version, 0, 105) && atLeastVersion(this.hass.config.version, 0, 105) &&
addons?.get(item.slug)?.icon addons?.get(item.slug)?.icon
? `/api/hassio/addons/${item.slug}/icon` ? `/api/hassio/addons/${item.slug}/icon`

View File

@@ -1,4 +1,4 @@
import { mdiArrowUpBoldCircle, mdiPuzzle } from "@mdi/js"; import { mdiArrowUpBoldCircle, mdiPlay, mdiPuzzle, mdiStop } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version"; import { atLeastVersion } from "../../../src/common/config/version";
@@ -17,7 +17,36 @@ class HassioAddons extends LitElement {
@property({ attribute: false }) public supervisor!: Supervisor; @property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow!: boolean;
protected render(): TemplateResult { protected render(): TemplateResult {
return html`<ha-card
.header=${this.supervisor.localize("dashboard.addons")}
>
<div class="addons" ?narrow=${this.narrow}>
${this.supervisor.supervisor.addons.map(
(addon) => html`<div
class="addon"
@click=${this._addonTapped}
.addon=${addon}
>
<div class="icon">
<div class="overlay">
<ha-svg-icon
.title=${addon.state}
.path=${addon.state === "started" ? mdiPlay : mdiStop}
>
</ha-svg-icon>
</div>
${addon.icon && atLeastVersion(this.hass.config.version, 0, 105)
? html`<img src="/api/hassio/addons/${addon.slug}/icon" />`
: html`<ha-svg-icon .path=${mdiPuzzle}></ha-svg-icon>`}
</div>
<div class="name">${addon.name}</div>
</div>`
)}
</div>
</ha-card>`;
return html` return html`
<div class="content"> <div class="content">
<h1>${this.supervisor.localize("dashboard.addons")}</h1> <h1>${this.supervisor.localize("dashboard.addons")}</h1>
@@ -88,9 +117,42 @@ class HassioAddons extends LitElement {
haStyle, haStyle,
hassioStyle, hassioStyle,
css` css`
ha-card { .addons {
display: grid;
grid-template-columns: repeat(4, auto);
padding-bottom: 16px;
}
.addons[narrow] {
grid-template-columns: repeat(2, auto);
}
.addon {
text-align: center;
max-width: 100px;
padding: 0 8px;
cursor: pointer; cursor: pointer;
} }
.icon > *:not(.overlay) {
position: relative;
max-height: 60px;
max-width: 60px;
margin: auto;
--mdc-icon-size: 60px;
display: flex;
}
.icon {
margin-bottom: 4px;
}
.overlay {
position: absolute;
z-index: 2;
--mdc-icon-size: 24px;
color: var(--secondary-text-color);
background-color: var(--secondary-background-color);
opacity: 0.6;
border-radius: 100%;
margin-left: 12px;
border: 1px var(--secondary-text-color) solid;
}
`, `,
]; ];
} }

View File

@@ -5,6 +5,7 @@ import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types"; import { HomeAssistant, Route } from "../../../src/types";
import { supervisorTabs } from "../hassio-tabs"; import { supervisorTabs } from "../hassio-tabs";
import { hassioStyle } from "../resources/hassio-style";
import "./hassio-addons"; import "./hassio-addons";
import "./hassio-update"; import "./hassio-update";
@@ -32,14 +33,17 @@ class HassioDashboard extends LitElement {
<span slot="header"> <span slot="header">
${this.supervisor.localize("panel.dashboard")} ${this.supervisor.localize("panel.dashboard")}
</span> </span>
<div class="content"> <div class="content">
<hassio-update <hassio-update
.hass=${this.hass} .hass=${this.hass}
.supervisor=${this.supervisor} .supervisor=${this.supervisor}
.narrow=${this.narrow}
></hassio-update> ></hassio-update>
<hassio-addons <hassio-addons
.hass=${this.hass} .hass=${this.hass}
.supervisor=${this.supervisor} .supervisor=${this.supervisor}
.narrow=${this.narrow}
></hassio-addons> ></hassio-addons>
</div> </div>
</hass-tabs-subpage> </hass-tabs-subpage>
@@ -49,9 +53,18 @@ class HassioDashboard extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,
hassioStyle,
css` css`
.content { .content {
margin: 0 auto; display: grid;
max-width: 1400px;
justify-content: center;
grid-template-columns: repeat(2, auto);
gap: 16px;
}
.content > * {
display: block;
min-width: 400px;
} }
`, `,
]; ];

View File

@@ -1,5 +1,5 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { mdiHomeAssistant } from "@mdi/js"; import { mdiHomeAssistant, mdiPuzzle } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@@ -42,11 +42,15 @@ export class HassioUpdate extends LitElement {
@property({ attribute: false }) public supervisor!: Supervisor; @property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow!: boolean;
private _pendingUpdates = memoizeOne( private _pendingUpdates = memoizeOne(
(supervisor: Supervisor): number => (supervisor: Supervisor): number =>
Object.keys(supervisor).filter( Object.keys(supervisor).filter(
(value) => supervisor[value].update_available (value) => supervisor[value].update_available
).length ).length +
supervisor.supervisor.addons.filter((addon) => addon.update_available)
.length
); );
protected render(): TemplateResult { protected render(): TemplateResult {
@@ -60,15 +64,38 @@ export class HassioUpdate extends LitElement {
} }
return html` return html`
<div class="content"> <ha-card
<h1> .header="${this.supervisor.localize(
${this.supervisor.localize( "common.update_available",
"common.update_available", "count",
"count", updatesAvailable + 1
updatesAvailable )}
🎉"
>
${this._renderUpdateRow({
type: "os",
heading: "Home Assistant Operating system",
icon: mdiHomeAssistant,
version: "5",
version_latest: "6",
})}
${this.supervisor.addon.addons
.filter((addon) => addon.update_available)
.map((addon) =>
this._renderUpdateRow({
type: "addon",
heading: addon.name,
version: addon.version_latest,
version_latest: addon.version,
image: addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined,
icon: mdiPuzzle,
})
)} )}
🎉 </ha-card>
</h1> <div class="content">
<h1></h1>
<div class="card-group"> <div class="card-group">
${this._renderUpdateCard( ${this._renderUpdateCard(
"Home Assistant Core", "Home Assistant Core",
@@ -86,7 +113,7 @@ export class HassioUpdate extends LitElement {
"hassio/supervisor/update", "hassio/supervisor/update",
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}` `https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}`
)} )}
${this.supervisor.host.features.includes("haos") ${this.supervisor.host.features.includes("hassos")
? this._renderUpdateCard( ? this._renderUpdateCard(
"Operating System", "Operating System",
"os", "os",
@@ -100,6 +127,37 @@ export class HassioUpdate extends LitElement {
`; `;
} }
private _renderUpdateRow(options: {
type: "supervisor" | "os" | "core" | "addon";
heading: string;
version: string;
version_latest: string;
icon?: string;
image?: string;
release_notes?: string;
slug?: string;
}): TemplateResult {
return html`<div class="update-row">
<paper-icon-item>
<div class="icon" slot="item-icon">
${options.image && atLeastVersion(this.hass.config.version, 0, 104)
? html`<img src="${options.image}" />`
: options.icon
? html`<ha-svg-icon .path=${options.icon}></ha-svg-icon>`
: ""}
</div>
<paper-item-body two-line>
${options.heading}
<div secondary>Version ${options.version_latest} is available</div>
</paper-item-body>
</paper-icon-item>
<div class="update-row-actions" ?narrow=${false}>
<mwc-button>Releaese notes</mwc-button>
<mwc-button>Update</mwc-button>
</div>
</div>`;
}
private _renderUpdateCard( private _renderUpdateCard(
name: string, name: string,
key: string, key: string,
@@ -231,31 +289,21 @@ export class HassioUpdate extends LitElement {
haStyle, haStyle,
hassioStyle, hassioStyle,
css` css`
.icon { .update-row,
--mdc-icon-size: 48px; paper-icon-item {
float: right; display: flex;
margin: 0 0 2px 10px; align-items: center;
color: var(--primary-text-color);
} }
.update-heading {
font-size: var(--paper-font-subhead_-_font-size); .update-row {
font-weight: 500; padding: 8px;
margin-bottom: 0.5em; justify-content: space-between;
color: var(--primary-text-color);
} }
.card-content { .icon > * {
height: calc(100% - 47px); max-height: 32px;
box-sizing: border-box; max-width: 32px;
} margin-right: 16px;
.card-actions { --mdc-icon-size: 32px;
text-align: right;
}
a {
text-decoration: none;
}
ha-settings-row {
padding: 0;
--paper-item-body-two-line-min-height: 32px;
} }
`, `,
]; ];

View File

@@ -1,194 +0,0 @@
import { mdiClose } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/common/search/search-input";
import { compare } from "../../../../src/common/string/compare";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
import { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
import { dump } from "../../../../src/resources/js-yaml-dump";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware";
const _filterDevices = memoizeOne(
(showAdvanced: boolean, hardware: HassioHardwareInfo, filter: string) =>
hardware.devices
.filter(
(device) =>
(showAdvanced ||
["tty", "gpio", "input"].includes(device.subsystem)) &&
(device.by_id?.toLowerCase().includes(filter) ||
device.name.toLowerCase().includes(filter) ||
device.dev_path.toLocaleLowerCase().includes(filter) ||
JSON.stringify(device.attributes)
.toLocaleLowerCase()
.includes(filter))
)
.sort((a, b) => compare(a.name, b.name))
);
@customElement("dialog-hassio-hardware")
class HassioHardwareDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _dialogParams?: HassioHardwareDialogParams;
@state() private _filter?: string;
public showDialog(params: HassioHardwareDialogParams) {
this._dialogParams = params;
}
public closeDialog() {
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._dialogParams) {
return html``;
}
const devices = _filterDevices(
this.hass.userData?.showAdvanced || false,
this._dialogParams.hardware,
(this._filter || "").toLowerCase()
);
return html`
<ha-dialog
open
scrimClickAction
hideActions
@closed=${this.closeDialog}
.heading=${true}
>
<div class="header" slot="heading">
<h2>
${this._dialogParams.supervisor.localize("dialog.hardware.title")}
</h2>
<mwc-icon-button dialogAction="close">
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
<search-input
autofocus
no-label-float
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
.label=${this._dialogParams.supervisor.localize(
"dialog.hardware.search"
)}
>
</search-input>
</div>
${devices.map(
(device) =>
html`<ha-expansion-panel
.header=${device.name}
.secondary=${device.by_id || undefined}
outlined
>
<div class="device-property">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.subsystem"
)}:
</span>
<span>${device.subsystem}</span>
</div>
<div class="device-property">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.device_path"
)}:
</span>
<code>${device.dev_path}</code>
</div>
${device.by_id
? html` <div class="device-property">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.id"
)}:
</span>
<code>${device.by_id}</code>
</div>`
: ""}
<div class="attributes">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.attributes"
)}:
</span>
<pre>${dump(device.attributes, { indent: 2 })}</pre>
</div>
</ha-expansion-panel>`
)}
</ha-dialog>
`;
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
mwc-icon-button {
position: absolute;
right: 16px;
top: 10px;
text-decoration: none;
color: var(--primary-text-color);
}
h2 {
margin: 18px 42px 0 18px;
color: var(--primary-text-color);
}
ha-expansion-panel {
margin: 4px 0;
}
pre,
code {
background-color: var(--markdown-code-background-color, none);
border-radius: 3px;
}
pre {
padding: 16px;
overflow: auto;
line-height: 1.45;
font-family: var(--code-font-family, monospace);
}
code {
font-size: 85%;
padding: 0.2em 0.4em;
}
search-input {
margin: 0 16px;
display: block;
}
.device-property {
display: flex;
justify-content: space-between;
}
.attributes {
margin-top: 12px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-hardware": HassioHardwareDialog;
}
}

View File

@@ -1,19 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioHardwareDialogParams {
supervisor: Supervisor;
hardware: HassioHardwareInfo;
}
export const showHassioHardwareDialog = (
element: HTMLElement,
dialogParams: HassioHardwareDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-hardware",
dialogImport: () => import("./dialog-hassio-hardware"),
dialogParams,
});
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -95,25 +95,16 @@ class HassioCreateSnapshotDialog extends LitElement {
this._creatingSnapshot = true; this._creatingSnapshot = true;
this._error = ""; this._error = "";
if (snapshotDetails.password && !snapshotDetails.password.length) { if (
this._snapshotContent.snapshotHasPassword &&
!this._snapshotContent.snapshotPassword.length
) {
this._error = this._dialogParams!.supervisor.localize( this._error = this._dialogParams!.supervisor.localize(
"snapshot.enter_password" "snapshot.enter_password"
); );
this._creatingSnapshot = false; this._creatingSnapshot = false;
return; return;
} }
if (
snapshotDetails.password &&
snapshotDetails.password !== snapshotDetails.confirm_password
) {
this._error = this._dialogParams!.supervisor.localize(
"snapshot.passwords_not_matching"
);
this._creatingSnapshot = false;
return;
}
delete snapshotDetails.confirm_password;
try { try {
if (this._snapshotContent.snapshotType === "full") { if (this._snapshotContent.snapshotType === "full") {

View File

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

View File

@@ -1,13 +1,13 @@
import { ActionDetail } from "@material/mwc-list"; import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiClose, mdiDotsVertical } from "@mdi/js"; import { mdiDotsVertical } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import { slugify } from "../../../../src/common/string/slugify"; import { slugify } from "../../../../src/common/string/slugify";
import "../../../../src/components/buttons/ha-progress-button"; import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-button-menu"; import "../../../../src/components/ha-button-menu";
import "../../../../src/components/ha-header-bar"; import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
import { getSignedPath } from "../../../../src/data/auth"; import { getSignedPath } from "../../../../src/data/auth";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
@@ -22,7 +22,6 @@ import {
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager"; import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import { fileDownload } from "../../../../src/util/file_download";
import "../../components/supervisor-snapshot-content"; import "../../components/supervisor-snapshot-content";
import type { SupervisorSnapshotContent } from "../../components/supervisor-snapshot-content"; import type { SupervisorSnapshotContent } from "../../components/supervisor-snapshot-content";
import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot"; import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot";
@@ -30,8 +29,7 @@ import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot";
@customElement("dialog-hassio-snapshot") @customElement("dialog-hassio-snapshot")
class HassioSnapshotDialog class HassioSnapshotDialog
extends LitElement extends LitElement
implements HassDialog<HassioSnapshotDialogParams> implements HassDialog<HassioSnapshotDialogParams> {
{
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@state() private _error?: string; @state() private _error?: string;
@@ -68,24 +66,14 @@ class HassioSnapshotDialog
open open
scrimClickAction scrimClickAction
@closed=${this.closeDialog} @closed=${this.closeDialog}
.heading=${true} .heading=${createCloseHeading(this.hass, this._computeName)}
> >
<div slot="heading">
<ha-header-bar>
<span slot="title">${this._snapshot.name}</span>
<mwc-icon-button slot="actionItems" dialogAction="cancel">
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
</ha-header-bar>
</div>
${this._restoringSnapshot ${this._restoringSnapshot
? html` <ha-circular-progress active></ha-circular-progress>` ? html` <ha-circular-progress active></ha-circular-progress>`
: html`<supervisor-snapshot-content : html`<supervisor-snapshot-content
.hass=${this.hass} .hass=${this.hass}
.supervisor=${this._dialogParams.supervisor} .supervisor=${this._dialogParams.supervisor}
.snapshot=${this._snapshot} .snapshot=${this._snapshot}
.onboarding=${this._dialogParams.onboarding || false}
.localize=${this._dialogParams.localize}
> >
</supervisor-snapshot-content>`} </supervisor-snapshot-content>`}
${this._error ? html`<p class="error">Error: ${this._error}</p>` : ""} ${this._error ? html`<p class="error">Error: ${this._error}</p>` : ""}
@@ -98,20 +86,18 @@ class HassioSnapshotDialog
Restore Restore
</mwc-button> </mwc-button>
${!this._dialogParams.onboarding <ha-button-menu
? html`<ha-button-menu fixed
fixed slot="primaryAction"
slot="primaryAction" @action=${this._handleMenuAction}
@action=${this._handleMenuAction} @closed=${(ev: Event) => ev.stopPropagation()}
@closed=${(ev: Event) => ev.stopPropagation()} >
> <mwc-icon-button slot="trigger" alt="menu">
<mwc-icon-button slot="trigger" alt="menu"> <ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon> </mwc-icon-button>
</mwc-icon-button> <mwc-list-item>Download Snapshot</mwc-list-item>
<mwc-list-item>Download Snapshot</mwc-list-item> <mwc-list-item class="error">Delete Snapshot</mwc-list-item>
<mwc-list-item class="error">Delete Snapshot</mwc-list-item> </ha-button-menu>
</ha-button-menu>`
: ""}
</ha-dialog> </ha-dialog>
`; `;
} }
@@ -128,12 +114,6 @@ class HassioSnapshotDialog
display: block; display: block;
text-align: center; text-align: center;
} }
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
display: block;
}
`, `,
]; ];
} }
@@ -298,7 +278,8 @@ class HassioSnapshotDialog
if (window.location.href.includes("ui.nabu.casa")) { if (window.location.href.includes("ui.nabu.casa")) {
const confirm = await showConfirmationDialog(this, { const confirm = await showConfirmationDialog(this, {
title: "Potential slow download", title: "Potential slow download",
text: "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", confirmText: "continue",
dismissText: "cancel", dismissText: "cancel",
}); });
@@ -307,11 +288,12 @@ class HassioSnapshotDialog
} }
} }
fileDownload( const a = document.createElement("a");
this, a.href = signedPath.path;
signedPath.path, a.download = `home_assistant_snapshot_${slugify(this._computeName)}.tar`;
`home_assistant_snapshot_${slugify(this._computeName)}.tar` this.shadowRoot!.appendChild(a);
); a.click();
this.shadowRoot!.removeChild(a);
} }
private get _computeName() { private get _computeName() {

View File

@@ -1,5 +1,4 @@
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import { LocalizeFunc } from "../../../../src/common/translations/localize";
import { Supervisor } from "../../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioSnapshotDialogParams { export interface HassioSnapshotDialogParams {
@@ -7,7 +6,6 @@ export interface HassioSnapshotDialogParams {
onDelete?: () => void; onDelete?: () => void;
onboarding?: boolean; onboarding?: boolean;
supervisor?: Supervisor; supervisor?: Supervisor;
localize?: LocalizeFunc;
} }
export const showHassioSnapshotDialog = ( export const showHassioSnapshotDialog = (

View File

@@ -49,9 +49,9 @@ class DialogSupervisorUpdate extends LitElement {
public focus(): void { public focus(): void {
this.updateComplete.then(() => this.updateComplete.then(() =>
( (this.shadowRoot?.querySelector(
this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement "[dialogInitialFocus]"
)?.focus() ) as HTMLElement)?.focus()
); );
} }
@@ -158,8 +158,8 @@ class DialogSupervisorUpdate extends LitElement {
} catch (err) { } catch (err) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) { if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err); this._error = extractApiErrorMessage(err);
this._action = null;
} }
this._action = null;
return; return;
} }

View File

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

View File

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

View File

@@ -97,23 +97,16 @@ class HassioIngressView extends LitElement {
title: requestedAddon, title: requestedAddon,
}); });
await nextRender(); await nextRender();
navigate("/hassio/store", { replace: true }); history.back();
return; return;
} }
if (!addonInfo.version) { if (!addonInfo.ingress) {
await showAlertDialog(this, {
text: this.supervisor.localize("my.error_addon_not_installed"),
title: addonInfo.name,
});
await nextRender();
navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true });
} else if (!addonInfo.ingress) {
await showAlertDialog(this, { await showAlertDialog(this, {
text: this.supervisor.localize("my.error_addon_no_ingress"), text: this.supervisor.localize("my.error_addon_no_ingress"),
title: addonInfo.name, title: addonInfo.name,
}); });
await nextRender(); await nextRender();
navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true }); history.back();
} else { } else {
navigate(`/hassio/ingress/${addonInfo.slug}`, { replace: true }); navigate(`/hassio/ingress/${addonInfo.slug}`, { replace: true });
} }

View File

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

View File

@@ -2,6 +2,7 @@ import "@material/mwc-button";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js"; import { mdiDotsVertical } from "@mdi/js";
import { dump } from "js-yaml";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@@ -40,8 +41,8 @@ import {
roundWithOneDecimal, roundWithOneDecimal,
} from "../../../src/util/calculate"; } from "../../../src/util/calculate";
import "../components/supervisor-metric"; import "../components/supervisor-metric";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
import { showNetworkDialog } from "../dialogs/network/show-dialog-network"; import { showNetworkDialog } from "../dialogs/network/show-dialog-network";
import { showHassioHardwareDialog } from "../dialogs/hardware/show-dialog-hassio-hardware";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-host-info") @customElement("hassio-host-info")
@@ -113,7 +114,7 @@ class HassioHostInfo extends LitElement {
` `
: ""} : ""}
</ha-settings-row> </ha-settings-row>
${!this.supervisor.host.features.includes("haos") ${!this.supervisor.host.features.includes("hassos")
? html`<ha-settings-row> ? html`<ha-settings-row>
<span slot="heading"> <span slot="heading">
${this.supervisor.localize("system.host.docker_version")} ${this.supervisor.localize("system.host.docker_version")}
@@ -190,7 +191,7 @@ class HassioHostInfo extends LitElement {
<mwc-list-item> <mwc-list-item>
${this.supervisor.localize("system.host.hardware")} ${this.supervisor.localize("system.host.hardware")}
</mwc-list-item> </mwc-list-item>
${this.supervisor.host.features.includes("haos") ${this.supervisor.host.features.includes("hassos")
? html`<mwc-list-item> ? html`<mwc-list-item>
${this.supervisor.localize("system.host.import_from_usb")} ${this.supervisor.localize("system.host.import_from_usb")}
</mwc-list-item>` </mwc-list-item>`
@@ -228,19 +229,20 @@ class HassioHostInfo extends LitElement {
} }
private async _showHardware(): Promise<void> { private async _showHardware(): Promise<void> {
let hardware;
try { try {
hardware = await fetchHassioHardwareInfo(this.hass); const content = await fetchHassioHardwareInfo(this.hass);
showHassioMarkdownDialog(this, {
title: this.supervisor.localize("system.host.hardware"),
content: `<pre>${dump(content, { indent: 2 })}</pre>`,
});
} catch (err) { } catch (err) {
await showAlertDialog(this, { showAlertDialog(this, {
title: this.supervisor.localize( title: this.supervisor.localize(
"system.host.failed_to_get_hardware_list" "system.host.failed_to_get_hardware_list"
), ),
text: extractApiErrorMessage(err), text: extractApiErrorMessage(err),
}); });
return;
} }
showHassioHardwareDialog(this, { supervisor: this.supervisor, hardware });
} }
private async _hostReboot(ev: CustomEvent): Promise<void> { private async _hostReboot(ev: CustomEvent): Promise<void> {

View File

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

View File

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

View File

@@ -9,6 +9,12 @@ if [ -z "${DEVCONTAINER}" ]; then
exit 1 exit 1
fi fi
if [ ! -z "${CODESPACES}" ]; then
WORKSPACE="/root/workspace/frontend"
else
WORKSPACE="/workspaces/frontend"
fi
if [ -z $(which hass) ]; then if [ -z $(which hass) ]; then
echo "Installing Home Asstant core from dev." echo "Installing Home Asstant core from dev."
python3 -m pip install --upgrade \ python3 -m pip install --upgrade \
@@ -16,9 +22,9 @@ if [ -z $(which hass) ]; then
git+git://github.com/home-assistant/home-assistant.git@dev git+git://github.com/home-assistant/home-assistant.git@dev
fi fi
if [ ! -d "/workspaces/frontend/config" ]; then if [ ! -d "${WORKSPACE}/config" ]; then
echo "Creating default configuration." echo "Creating default configuration."
mkdir -p "/workspaces/frontend/config"; mkdir -p "${WORKSPACE}/config";
hass --script ensure_config -c config hass --script ensure_config -c config
echo "demo: echo "demo:
@@ -26,24 +32,24 @@ logger:
default: info default: info
logs: logs:
homeassistant.components.frontend: debug homeassistant.components.frontend: debug
" >> /workspaces/frontend/config/configuration.yaml " >> "${WORKSPACE}/config/configuration.yaml"
if [ ! -z "${HASSIO}" ]; then if [ ! -z "${HASSIO}" ]; then
echo " echo "
# frontend: # frontend:
# development_repo: /workspaces/frontend # development_repo: ${WORKSPACE}
hassio: hassio:
development_repo: /workspaces/frontend" >> /workspaces/frontend/config/configuration.yaml development_repo: ${WORKSPACE}" >> "${WORKSPACE}/config/configuration.yaml"
else else
echo " echo "
frontend: frontend:
development_repo: /workspaces/frontend development_repo: ${WORKSPACE}
# hassio: # hassio:
# development_repo: /workspaces/frontend" >> /workspaces/frontend/config/configuration.yaml # development_repo: ${WORKSPACE}" >> "${WORKSPACE}/config/configuration.yaml"
fi fi
fi fi
hass -c /workspaces/frontend/config hass -c "${WORKSPACE}/config"

View File

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

View File

@@ -7,7 +7,6 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import "./ha-password-manager-polyfill";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import "../components/ha-form/ha-form"; import "../components/ha-form/ha-form";
import "../components/ha-markdown"; import "../components/ha-markdown";
@@ -21,7 +20,7 @@ import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
type State = "loading" | "error" | "step"; type State = "loading" | "error" | "step";
class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
@property({ attribute: false }) public authProvider?: AuthProvider; @property() public authProvider?: AuthProvider;
@property() public clientId?: string; @property() public clientId?: string;
@@ -38,15 +37,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
@state() private _errorMessage?: string; @state() private _errorMessage?: string;
protected render() { protected render() {
return html` return html` <form>${this._renderForm()}</form> `;
<form>${this._renderForm()}</form>
<ha-password-manager-polyfill
.step=${this._step}
.stepData=${this._stepData}
@form-submitted=${this._handleSubmit}
@value-changed=${this._stepDataChanged}
></ha-password-manager-polyfill>
`;
} }
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
@@ -240,17 +231,11 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
await this.updateComplete; await this.updateComplete;
// 100ms to give all the form elements time to initialize. // 100ms to give all the form elements time to initialize.
setTimeout(() => { setTimeout(() => {
const form = this.renderRoot.querySelector("ha-form"); const form = this.shadowRoot!.querySelector("ha-form");
if (form) { if (form) {
(form as any).focus(); (form as any).focus();
} }
}, 100); }, 100);
setTimeout(() => {
this.renderRoot.querySelector(
"ha-password-manager-polyfill"
)!.boundingRect = this.getBoundingClientRect();
}, 500);
} }
private _stepDataChanged(ev: CustomEvent) { private _stepDataChanged(ev: CustomEvent) {
@@ -344,9 +329,3 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
} }
} }
customElements.define("ha-auth-flow", HaAuthFlow); customElements.define("ha-auth-flow", HaAuthFlow);
declare global {
interface HTMLElementTagNameMap {
"ha-auth-flow": HaAuthFlow;
}
}

View File

@@ -1,110 +0,0 @@
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { HaFormSchema } from "../components/ha-form/ha-form";
import { DataEntryFlowStep } from "../data/data_entry_flow";
declare global {
interface HTMLElementTagNameMap {
"ha-password-manager-polyfill": HaPasswordManagerPolyfill;
}
interface HASSDomEvents {
"form-submitted": undefined;
}
}
const ENABLED_HANDLERS = [
"homeassistant",
"legacy_api_password",
"command_line",
];
@customElement("ha-password-manager-polyfill")
export class HaPasswordManagerPolyfill extends LitElement {
@property({ attribute: false }) public step?: DataEntryFlowStep;
@property({ attribute: false }) public stepData: any;
@property({ attribute: false }) public boundingRect?: DOMRect;
protected createRenderRoot() {
// Add under document body so the element isn't placed inside any shadow roots
return document.body;
}
private get styles() {
return `
.password-manager-polyfill {
position: absolute;
top: ${this.boundingRect?.y || 148}px;
left: calc(50% - ${(this.boundingRect?.width || 360) / 2}px);
width: ${this.boundingRect?.width || 360}px;
opacity: 0;
z-index: -1;
}
.password-manager-polyfill input {
width: 100%;
height: 62px;
padding: 0;
border: 0;
}
.password-manager-polyfill input[type="submit"] {
width: 0;
height: 0;
}
`;
}
protected render(): TemplateResult {
if (
this.step &&
this.step.type === "form" &&
this.step.step_id === "init" &&
ENABLED_HANDLERS.includes(this.step.handler[0])
) {
return html`
<form
class="password-manager-polyfill"
aria-hidden="true"
@submit=${this._handleSubmit}
>
${this.step.data_schema.map((input) => this.render_input(input))}
<input type="submit" />
<style>
${this.styles}
</style>
</form>
`;
}
return html``;
}
private render_input(schema: HaFormSchema): TemplateResult | string {
const inputType = schema.name.includes("password") ? "password" : "text";
if (schema.type !== "string") {
return "";
}
return html`
<input
tabindex="-1"
.id=${schema.name}
.type=${inputType}
.value=${this.stepData[schema.name] || ""}
@input=${this._valueChanged}
/>
`;
}
private _handleSubmit(ev: Event) {
ev.preventDefault();
fireEvent(this, "form-submitted");
}
private _valueChanged(ev: Event) {
const target = ev.target! as HTMLInputElement;
this.stepData = { ...this.stepData, [target.id]: target.value };
fireEvent(this, "value-changed", {
value: this.stepData,
});
}
}

View File

@@ -1,63 +0,0 @@
export const COLORS = [
"#377eb8",
"#984ea3",
"#00d2d5",
"#ff7f00",
"#af8d00",
"#7f80cd",
"#b3e900",
"#c42e60",
"#a65628",
"#f781bf",
"#8dd3c7",
"#bebada",
"#fb8072",
"#80b1d3",
"#fdb462",
"#fccde5",
"#bc80bd",
"#ffed6f",
"#c4eaff",
"#cf8c00",
"#1b9e77",
"#d95f02",
"#e7298a",
"#e6ab02",
"#a6761d",
"#0097ff",
"#00d067",
"#f43600",
"#4ba93b",
"#5779bb",
"#927acc",
"#97ee3f",
"#bf3947",
"#9f5b00",
"#f48758",
"#8caed6",
"#f2b94f",
"#eff26e",
"#e43872",
"#d9b100",
"#9d7a00",
"#698cff",
"#d9d9d9",
"#00d27e",
"#d06800",
"#009f82",
"#c49200",
"#cbe8ff",
"#fecddf",
"#c27eb6",
"#8cd2ce",
"#c4b8d9",
"#f883b0",
"#a49100",
"#f48800",
"#27d0df",
"#a04a9b",
];
export function getColorByIndex(index: number) {
return COLORS[index % COLORS.length];
}

View File

@@ -1,4 +1,4 @@
export const luminosity = (rgb: [number, number, number]): number => { const luminosity = (rgb: [number, number, number]): number => {
// http://www.w3.org/TR/WCAG20/#relativeluminancedef // http://www.w3.org/TR/WCAG20/#relativeluminancedef
const lum: [number, number, number] = [0, 0, 0]; const lum: [number, number, number] = [0, 0, 0];
for (let i = 0; i < rgb.length; i++) { for (let i = 0; i < rgb.length; i++) {

View File

@@ -42,7 +42,6 @@ export const FIXED_DOMAIN_ICONS = {
remote: "hass:remote", remote: "hass:remote",
scene: "hass:palette", scene: "hass:palette",
script: "hass:script-text", script: "hass:script-text",
select: "hass:format-list-bulleted",
sensor: "hass:eye", sensor: "hass:eye",
simple_alarm: "hass:bell", simple_alarm: "hass:bell",
sun: "hass:white-balance-sunny", sun: "hass:white-balance-sunny",
@@ -59,11 +58,10 @@ export const FIXED_DEVICE_CLASS_ICONS = {
current: "hass:current-ac", current: "hass:current-ac",
carbon_dioxide: "mdi:molecule-co2", carbon_dioxide: "mdi:molecule-co2",
carbon_monoxide: "mdi:molecule-co", carbon_monoxide: "mdi:molecule-co",
energy: "hass:lightning-bolt", energy: "hass:flash",
humidity: "hass:water-percent", humidity: "hass:water-percent",
illuminance: "hass:brightness-5", illuminance: "hass:brightness-5",
temperature: "hass:thermometer", temperature: "hass:thermometer",
monetary: "mdi:cash",
pressure: "hass:gauge", pressure: "hass:gauge",
power: "hass:flash", power: "hass:flash",
power_factor: "hass:angle-acute", power_factor: "hass:angle-acute",
@@ -85,7 +83,6 @@ export const DOMAINS_WITH_CARD = [
"number", "number",
"scene", "scene",
"script", "script",
"select",
"timer", "timer",
"vacuum", "vacuum",
"water_heater", "water_heater",
@@ -124,7 +121,6 @@ export const DOMAINS_HIDE_MORE_INFO = [
"input_text", "input_text",
"number", "number",
"scene", "scene",
"select",
]; ];
/** Domains that should have the history hidden in the more info dialog. */ /** Domains that should have the history hidden in the more info dialog. */

View File

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

View File

@@ -17,19 +17,6 @@ export const formatDate = toLocaleDateStringSupportsOptions
formatDateMem(locale).format(dateObj) formatDateMem(locale).format(dateObj)
: (dateObj: Date) => format(dateObj, "longDate"); : (dateObj: Date) => format(dateObj, "longDate");
const formatDateShortMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
day: "numeric",
month: "short",
})
);
export const formatDateShort = toLocaleDateStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatDateShortMem(locale).format(dateObj)
: (dateObj: Date) => format(dateObj, "shortDate");
const formatDateWeekdayMem = memoizeOne( const formatDateWeekdayMem = memoizeOne(
(locale: FrontendLocaleData) => (locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, { new Intl.DateTimeFormat(locale.language, {

View File

@@ -82,71 +82,67 @@ class Storage {
const storage = new Storage(); const storage = new Storage();
export const LocalStorage = export const LocalStorage = (
( storageKey?: string,
storageKey?: string, property?: boolean,
property?: boolean, propertyOptions?: PropertyDeclaration
propertyOptions?: PropertyDeclaration ): any => (clsElement: ClassElement) => {
): any => const key = String(clsElement.key);
(clsElement: ClassElement) => { storageKey = storageKey || String(clsElement.key);
const key = String(clsElement.key); const initVal = clsElement.initializer ? clsElement.initializer() : undefined;
storageKey = storageKey || String(clsElement.key);
const initVal = clsElement.initializer
? clsElement.initializer()
: undefined;
storage.addFromStorage(storageKey); storage.addFromStorage(storageKey);
const subscribe = (el: ReactiveElement): UnsubscribeFunc => const subscribe = (el: ReactiveElement): UnsubscribeFunc =>
storage.subscribeChanges(storageKey!, (oldValue) => { storage.subscribeChanges(storageKey!, (oldValue) => {
el.requestUpdate(clsElement.key, oldValue); el.requestUpdate(clsElement.key, oldValue);
}); });
const getValue = (): any => const getValue = (): any =>
storage.hasKey(storageKey!) ? storage.getValue(storageKey!) : initVal; storage.hasKey(storageKey!) ? storage.getValue(storageKey!) : initVal;
const setValue = (el: ReactiveElement, value: any) => { const setValue = (el: ReactiveElement, value: any) => {
let oldValue: unknown | undefined; let oldValue: unknown | undefined;
if (property) { if (property) {
oldValue = getValue(); oldValue = getValue();
} }
storage.setValue(storageKey!, value); storage.setValue(storageKey!, value);
if (property) { if (property) {
el.requestUpdate(clsElement.key, oldValue); el.requestUpdate(clsElement.key, oldValue);
} }
};
return {
kind: "method",
placement: "prototype",
key: clsElement.key,
descriptor: {
set(this: ReactiveElement, value: unknown) {
setValue(this, value);
},
get() {
return getValue();
},
enumerable: true,
configurable: true,
},
finisher(cls: typeof ReactiveElement) {
if (property) {
const connectedCallback = cls.prototype.connectedCallback;
const disconnectedCallback = cls.prototype.disconnectedCallback;
cls.prototype.connectedCallback = function () {
connectedCallback.call(this);
this[`__unbsubLocalStorage${key}`] = subscribe(this);
};
cls.prototype.disconnectedCallback = function () {
disconnectedCallback.call(this);
this[`__unbsubLocalStorage${key}`]();
};
cls.createProperty(clsElement.key, {
noAccessor: true,
...propertyOptions,
});
}
},
};
}; };
return {
kind: "method",
placement: "prototype",
key: clsElement.key,
descriptor: {
set(this: ReactiveElement, value: unknown) {
setValue(this, value);
},
get() {
return getValue();
},
enumerable: true,
configurable: true,
},
finisher(cls: typeof ReactiveElement) {
if (property) {
const connectedCallback = cls.prototype.connectedCallback;
const disconnectedCallback = cls.prototype.disconnectedCallback;
cls.prototype.connectedCallback = function () {
connectedCallback.call(this);
this[`__unbsubLocalStorage${key}`] = subscribe(this);
};
cls.prototype.disconnectedCallback = function () {
disconnectedCallback.call(this);
this[`__unbsubLocalStorage${key}`]();
};
cls.createProperty(clsElement.key, {
noAccessor: true,
...propertyOptions,
});
}
},
};
};

View File

@@ -1,33 +1,33 @@
import type { LitElement } from "lit"; import type { LitElement } from "lit";
import type { ClassElement } from "../../types"; import type { ClassElement } from "../../types";
export const restoreScroll = export const restoreScroll = (selector: string): any => (
(selector: string): any => element: ClassElement
(element: ClassElement) => ({ ) => ({
kind: "method", kind: "method",
placement: "prototype", placement: "prototype",
key: element.key, key: element.key,
descriptor: { descriptor: {
set(this: LitElement, value: number) { set(this: LitElement, value: number) {
this[`__${String(element.key)}`] = value; this[`__${String(element.key)}`] = value;
},
get(this: LitElement) {
return this[`__${String(element.key)}`];
},
enumerable: true,
configurable: true,
}, },
finisher(cls: typeof LitElement) { get(this: LitElement) {
const connectedCallback = cls.prototype.connectedCallback; return this[`__${String(element.key)}`];
cls.prototype.connectedCallback = function () { },
connectedCallback.call(this); enumerable: true,
if (this[element.key]) { configurable: true,
const target = this.renderRoot.querySelector(selector); },
if (!target) { finisher(cls: typeof LitElement) {
return; const connectedCallback = cls.prototype.connectedCallback;
} cls.prototype.connectedCallback = function () {
target.scrollTop = this[element.key]; connectedCallback.call(this);
if (this[element.key]) {
const target = this.renderRoot.querySelector(selector);
if (!target) {
return;
} }
}; target.scrollTop = this[element.key];
}, }
}); };
},
});

View File

@@ -6,7 +6,8 @@ export type LeafletDrawModuleType = typeof import("leaflet-draw");
export const setupLeafletMap = async ( export const setupLeafletMap = async (
mapElement: HTMLElement, mapElement: HTMLElement,
darkMode?: boolean darkMode?: boolean,
draw = false
): Promise<[Map, LeafletModuleType, TileLayer]> => { ): Promise<[Map, LeafletModuleType, TileLayer]> => {
if (!mapElement.parentNode) { if (!mapElement.parentNode) {
throw new Error("Cannot setup Leaflet map on disconnected element"); throw new Error("Cannot setup Leaflet map on disconnected element");
@@ -16,6 +17,10 @@ export const setupLeafletMap = async (
.default as LeafletModuleType; .default as LeafletModuleType;
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/"; Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
if (draw) {
await import("leaflet-draw");
}
const map = Leaflet.map(mapElement); const map = Leaflet.map(mapElement);
const style = document.createElement("link"); const style = document.createElement("link");
style.setAttribute("href", "/static/images/leaflet/leaflet.css"); style.setAttribute("href", "/static/images/leaflet/leaflet.css");

View File

@@ -21,16 +21,6 @@ export const computeStateDisplay = (
} }
if (stateObj.attributes.unit_of_measurement) { if (stateObj.attributes.unit_of_measurement) {
if (stateObj.attributes.device_class === "monetary") {
try {
return formatNumber(compareState, locale, {
style: "currency",
currency: stateObj.attributes.unit_of_measurement,
});
} catch (_err) {
// fallback to default
}
}
return `${formatNumber(compareState, locale)} ${ return `${formatNumber(compareState, locale)} ${
stateObj.attributes.unit_of_measurement stateObj.attributes.unit_of_measurement
}`; }`;
@@ -39,61 +29,37 @@ export const computeStateDisplay = (
const domain = computeStateDomain(stateObj); const domain = computeStateDomain(stateObj);
if (domain === "input_datetime") { if (domain === "input_datetime") {
if (state) { let date: Date;
// If trying to display an explicit state, need to parse the explict state to `Date` then format. if (!stateObj.attributes.has_time) {
// Attributes aren't available, we have to use `state`.
try {
const components = state.split(" ");
if (components.length === 2) {
// Date and time.
return formatDateTime(new Date(components.join("T")), locale);
}
if (components.length === 1) {
if (state.includes("-")) {
// Date only.
return formatDate(new Date(`${state}T00:00`), locale);
}
if (state.includes(":")) {
// Time only.
const now = new Date();
return formatTime(
new Date(`${now.toISOString().split("T")[0]}T${state}`),
locale
);
}
}
return state;
} catch {
// Formatting methods may throw error if date parsing doesn't go well,
// just return the state string in that case.
return state;
}
} else {
// If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format.
let date: Date;
if (!stateObj.attributes.has_time) {
date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day
);
return formatDate(date, locale);
}
if (!stateObj.attributes.has_date) {
date = new Date();
date.setHours(stateObj.attributes.hour, stateObj.attributes.minute);
return formatTime(date, locale);
}
date = new Date( date = new Date(
stateObj.attributes.year, stateObj.attributes.year,
stateObj.attributes.month - 1, stateObj.attributes.month - 1,
stateObj.attributes.day, stateObj.attributes.day
);
return formatDate(date, locale);
}
if (!stateObj.attributes.has_date) {
const now = new Date();
date = new Date(
// Due to bugs.chromium.org/p/chromium/issues/detail?id=797548
// don't use artificial 1970 year.
now.getFullYear(),
now.getMonth(),
now.getDay(),
stateObj.attributes.hour, stateObj.attributes.hour,
stateObj.attributes.minute stateObj.attributes.minute
); );
return formatDateTime(date, locale); return formatTime(date, locale);
} }
date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day,
stateObj.attributes.hour,
stateObj.attributes.minute
);
return formatDateTime(date, locale);
} }
if (domain === "humidifier") { if (domain === "humidifier") {

View File

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

View File

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

View File

@@ -1,2 +0,0 @@
export const clamp = (value: number, min: number, max: number) =>
Math.min(Math.max(value, min), max);

View File

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

View File

@@ -1,16 +1,9 @@
import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMagnify } from "@mdi/js"; import { mdiClose, mdiMagnify } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { import { customElement, property } from "lit/decorators";
css, import { classMap } from "lit/directives/class-map";
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../components/ha-svg-icon"; import "../../components/ha-svg-icon";
import { fireEvent } from "../dom/fire_event"; import { fireEvent } from "../dom/fire_event";
@@ -34,11 +27,18 @@ class SearchInput extends LitElement {
this.shadowRoot!.querySelector("paper-input")!.focus(); this.shadowRoot!.querySelector("paper-input")!.focus();
} }
@query("paper-input", true) private _input!: PaperInputElement;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<style>
.no-underline:not(.focused) {
--paper-input-container-underline: {
display: none;
height: 0;
}
}
</style>
<paper-input <paper-input
class=${classMap({ "no-underline": this.noUnderline })}
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.label=${this.label || "Search"} .label=${this.label || "Search"}
.value=${this.filter} .value=${this.filter}
@@ -62,19 +62,6 @@ class SearchInput extends LitElement {
`; `;
} }
protected updated(changedProps: PropertyValues) {
if (
changedProps.has("noUnderline") &&
(this.noUnderline || changedProps.get("noUnderline") !== undefined)
) {
(
this._input.inputElement!.parentElement!.shadowRoot!.querySelector(
"div.unfocused-line"
) as HTMLElement
).style.display = this.noUnderline ? "none" : "block";
}
}
private async _filterChanged(value: string) { private async _filterChanged(value: string) {
fireEvent(this, "value-changed", { value: String(value) }); fireEvent(this, "value-changed", { value: String(value) });
} }

View File

@@ -1,22 +1,4 @@
import { FrontendLocaleData, NumberFormat } from "../../data/translation"; import { FrontendLocaleData, NumberFormat } from "../../data/translation";
import { round } from "../number/round";
export const numberFormatToLocale = (
localeOptions: FrontendLocaleData
): string | string[] | undefined => {
switch (localeOptions.number_format) {
case NumberFormat.comma_decimal:
return ["en-US", "en"]; // Use United States with fallback to English formatting 1,234,567.89
case NumberFormat.decimal_comma:
return ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89
case NumberFormat.space_comma:
return ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89
case NumberFormat.system:
return undefined;
default:
return localeOptions.language;
}
};
/** /**
* Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility. * Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility.
@@ -27,12 +9,27 @@ export const numberFormatToLocale = (
*/ */
export const formatNumber = ( export const formatNumber = (
num: string | number, num: string | number,
localeOptions?: FrontendLocaleData, locale?: FrontendLocaleData,
options?: Intl.NumberFormatOptions options?: Intl.NumberFormatOptions
): string => { ): string => {
const locale = localeOptions let format: string | string[] | undefined;
? numberFormatToLocale(localeOptions)
: undefined; switch (locale?.number_format) {
case NumberFormat.comma_decimal:
format = ["en-US", "en"]; // Use United States with fallback to English formatting 1,234,567.89
break;
case NumberFormat.decimal_comma:
format = ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89
break;
case NumberFormat.space_comma:
format = ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89
break;
case NumberFormat.system:
format = undefined;
break;
default:
format = locale?.language;
}
// Polyfill for Number.isNaN, which is more reliable than the global isNaN() // Polyfill for Number.isNaN, which is more reliable than the global isNaN()
Number.isNaN = Number.isNaN =
@@ -42,13 +39,13 @@ export const formatNumber = (
}; };
if ( if (
localeOptions?.number_format !== NumberFormat.none &&
!Number.isNaN(Number(num)) && !Number.isNaN(Number(num)) &&
Intl Intl &&
locale?.number_format !== NumberFormat.none
) { ) {
try { try {
return new Intl.NumberFormat( return new Intl.NumberFormat(
locale, format,
getDefaultFormatOptions(num, options) getDefaultFormatOptions(num, options)
).format(Number(num)); ).format(Number(num));
} catch (error) { } catch (error) {
@@ -61,12 +58,7 @@ export const formatNumber = (
).format(Number(num)); ).format(Number(num));
} }
} }
if (typeof num === "string") { return num.toString();
return num;
}
return `${round(num, options?.maximumFractionDigits).toString()}${
options?.style === "currency" ? ` ${options.currency}` : ""
}`;
}; };
/** /**
@@ -78,10 +70,7 @@ const getDefaultFormatOptions = (
num: string | number, num: string | number,
options?: Intl.NumberFormatOptions options?: Intl.NumberFormatOptions
): Intl.NumberFormatOptions => { ): Intl.NumberFormatOptions => {
const defaultOptions: Intl.NumberFormatOptions = { const defaultOptions: Intl.NumberFormatOptions = options || {};
maximumFractionDigits: 2,
...options,
};
if (typeof num !== "string") { if (typeof num !== "string") {
return defaultOptions; return defaultOptions;

View File

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

View File

@@ -29,28 +29,31 @@ export const iconColorCSS = css`
} }
ha-icon[data-domain="climate"][data-state="cooling"] { ha-icon[data-domain="climate"][data-state="cooling"] {
color: var(--cool-color, var(--state-climate-cool-color)); color: var(--cool-color, #2b9af9);
} }
ha-icon[data-domain="climate"][data-state="heating"] { ha-icon[data-domain="climate"][data-state="heating"] {
color: var(--heat-color, var(--state-climate-heat-color)); color: var(--heat-color, #ff8100);
} }
ha-icon[data-domain="climate"][data-state="drying"] { ha-icon[data-domain="climate"][data-state="drying"] {
color: var(--dry-color, var(--state-climate-dry-color)); color: var(--dry-color, #efbd07);
} }
ha-icon[data-domain="alarm_control_panel"] { ha-icon[data-domain="alarm_control_panel"] {
color: var(--alarm-color-armed, var(--label-badge-red)); color: var(--alarm-color-armed, var(--label-badge-red));
} }
ha-icon[data-domain="alarm_control_panel"][data-state="disarmed"] { ha-icon[data-domain="alarm_control_panel"][data-state="disarmed"] {
color: var(--alarm-color-disarmed, var(--label-badge-green)); color: var(--alarm-color-disarmed, var(--label-badge-green));
} }
ha-icon[data-domain="alarm_control_panel"][data-state="pending"], ha-icon[data-domain="alarm_control_panel"][data-state="pending"],
ha-icon[data-domain="alarm_control_panel"][data-state="arming"] { ha-icon[data-domain="alarm_control_panel"][data-state="arming"] {
color: var(--alarm-color-pending, var(--label-badge-yellow)); color: var(--alarm-color-pending, var(--label-badge-yellow));
animation: pulse 1s infinite; animation: pulse 1s infinite;
} }
ha-icon[data-domain="alarm_control_panel"][data-state="triggered"] { ha-icon[data-domain="alarm_control_panel"][data-state="triggered"] {
color: var(--alarm-color-triggered, var(--label-badge-red)); color: var(--alarm-color-triggered, var(--label-badge-red));
animation: pulse 1s infinite; animation: pulse 1s infinite;
@@ -70,11 +73,11 @@ export const iconColorCSS = css`
ha-icon[data-domain="plant"][data-state="problem"], ha-icon[data-domain="plant"][data-state="problem"],
ha-icon[data-domain="zwave"][data-state="dead"] { ha-icon[data-domain="zwave"][data-state="dead"] {
color: var(--state-icon-error-color); color: var(--error-state-color, #db4437);
} }
/* Color the icon if unavailable */ /* Color the icon if unavailable */
ha-icon[data-state="unavailable"] { ha-icon[data-state="unavailable"] {
color: var(--state-unavailable-color); color: var(--state-icon-unavailable-color);
} }
`; `;

View File

@@ -5,20 +5,32 @@
// as much as it can, without ever going more than once per `wait` duration; // as much as it can, without ever going more than once per `wait` duration;
// but if you'd like to disable the execution on the leading edge, pass // but if you'd like to disable the execution on the leading edge, pass
// `false for leading`. To disable execution on the trailing edge, ditto. // `false for leading`. To disable execution on the trailing edge, ditto.
export const throttle = <T extends any[]>( export const throttle = <T extends (...args) => unknown>(
func: (...args: T) => void, func: T,
wait: number, wait: number,
leading = true, leading = true,
trailing = true trailing = true
) => { ): T => {
let timeout: number | undefined; let timeout: number | undefined;
let previous = 0; let previous = 0;
return (...args: T): void => { let context: any;
const later = () => { let args: any;
previous = leading === false ? 0 : Date.now(); const later = () => {
timeout = undefined; previous = leading === false ? 0 : Date.now();
func(...args); timeout = undefined;
}; func.apply(context, args);
if (!timeout) {
context = null;
args = null;
}
};
// @ts-ignore
return function (...argmnts) {
// @ts-ignore
// @typescript-eslint/no-this-alias
context = this;
args = argmnts;
const now = Date.now(); const now = Date.now();
if (!previous && leading === false) { if (!previous && leading === false) {
previous = now; previous = now;
@@ -30,7 +42,7 @@ export const throttle = <T extends any[]>(
timeout = undefined; timeout = undefined;
} }
previous = now; previous = now;
func(...args); func.apply(context, args);
} else if (!timeout && trailing !== false) { } else if (!timeout && trailing !== false) {
timeout = window.setTimeout(later, remaining); timeout = window.setTimeout(later, remaining);
} }

View File

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

View File

@@ -5,7 +5,7 @@ import { customElement, property, query } from "lit/decorators";
import "../ha-circular-progress"; import "../ha-circular-progress";
@customElement("ha-progress-button") @customElement("ha-progress-button")
export class HaProgressButton extends LitElement { class HaProgressButton extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public progress = false; @property({ type: Boolean }) public progress = false;

View File

@@ -1,197 +0,0 @@
import { _adapters } from "chart.js";
import {
startOfSecond,
startOfMinute,
startOfHour,
startOfDay,
startOfWeek,
startOfMonth,
startOfQuarter,
startOfYear,
addMilliseconds,
addSeconds,
addMinutes,
addHours,
addDays,
addWeeks,
addMonths,
addQuarters,
addYears,
differenceInMilliseconds,
differenceInSeconds,
differenceInMinutes,
differenceInHours,
differenceInDays,
differenceInWeeks,
differenceInMonths,
differenceInQuarters,
differenceInYears,
endOfSecond,
endOfMinute,
endOfHour,
endOfDay,
endOfWeek,
endOfMonth,
endOfQuarter,
endOfYear,
} from "date-fns";
import { formatDate, formatDateShort } from "../../common/datetime/format_date";
import {
formatDateTime,
formatDateTimeWithSeconds,
} from "../../common/datetime/format_date_time";
import {
formatTime,
formatTimeWithSeconds,
} from "../../common/datetime/format_time";
const FORMATS = {
datetime: "datetime",
datetimeseconds: "datetimeseconds",
millisecond: "millisecond",
second: "second",
minute: "minute",
hour: "hour",
day: "day",
week: "week",
month: "month",
quarter: "quarter",
year: "year",
};
_adapters._date.override({
formats: () => FORMATS,
parse: (value: Date | number) => {
if (!(value instanceof Date)) {
return value;
}
return value.getTime();
},
format: function (time, fmt: keyof typeof FORMATS) {
switch (fmt) {
case "datetime":
return formatDateTime(new Date(time), this.options.locale);
case "datetimeseconds":
return formatDateTimeWithSeconds(new Date(time), this.options.locale);
case "millisecond":
return formatTimeWithSeconds(new Date(time), this.options.locale);
case "second":
return formatTimeWithSeconds(new Date(time), this.options.locale);
case "minute":
return formatTime(new Date(time), this.options.locale);
case "hour":
return formatTime(new Date(time), this.options.locale);
case "day":
return formatDateShort(new Date(time), this.options.locale);
case "week":
return formatDate(new Date(time), this.options.locale);
case "month":
return formatDate(new Date(time), this.options.locale);
case "quarter":
return formatDate(new Date(time), this.options.locale);
case "year":
return formatDate(new Date(time), this.options.locale);
default:
return "";
}
},
// @ts-ignore
add: (time, amount, unit) => {
switch (unit) {
case "millisecond":
return addMilliseconds(time, amount);
case "second":
return addSeconds(time, amount);
case "minute":
return addMinutes(time, amount);
case "hour":
return addHours(time, amount);
case "day":
return addDays(time, amount);
case "week":
return addWeeks(time, amount);
case "month":
return addMonths(time, amount);
case "quarter":
return addQuarters(time, amount);
case "year":
return addYears(time, amount);
default:
return time;
}
},
diff: (max, min, unit) => {
switch (unit) {
case "millisecond":
return differenceInMilliseconds(max, min);
case "second":
return differenceInSeconds(max, min);
case "minute":
return differenceInMinutes(max, min);
case "hour":
return differenceInHours(max, min);
case "day":
return differenceInDays(max, min);
case "week":
return differenceInWeeks(max, min);
case "month":
return differenceInMonths(max, min);
case "quarter":
return differenceInQuarters(max, min);
case "year":
return differenceInYears(max, min);
default:
return 0;
}
},
// @ts-ignore
startOf: (time, unit, weekday) => {
switch (unit) {
case "second":
return startOfSecond(time);
case "minute":
return startOfMinute(time);
case "hour":
return startOfHour(time);
case "day":
return startOfDay(time);
case "week":
return startOfWeek(time);
case "isoWeek":
return startOfWeek(time, {
weekStartsOn: +weekday! as 0 | 1 | 2 | 3 | 4 | 5 | 6,
});
case "month":
return startOfMonth(time);
case "quarter":
return startOfQuarter(time);
case "year":
return startOfYear(time);
default:
return time;
}
},
// @ts-ignore
endOf: (time, unit) => {
switch (unit) {
case "second":
return endOfSecond(time);
case "minute":
return endOfMinute(time);
case "hour":
return endOfHour(time);
case "day":
return endOfDay(time);
case "week":
return endOfWeek(time);
case "month":
return endOfMonth(time);
case "quarter":
return endOfQuarter(time);
case "year":
return endOfYear(time);
default:
return time;
}
},
});

View File

@@ -1,349 +0,0 @@
import type {
Chart,
ChartType,
ChartData,
ChartOptions,
TooltipModel,
} from "chart.js";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { clamp } from "../../common/number/clamp";
interface Tooltip extends TooltipModel<any> {
top: string;
left: string;
}
@customElement("ha-chart-base")
export default class HaChartBase extends LitElement {
public chart?: Chart;
@property({ attribute: "chart-type", reflect: true })
public chartType: ChartType = "line";
@property({ attribute: false }) public data: ChartData = { datasets: [] };
@property({ attribute: false }) public options?: ChartOptions;
@property({ attribute: false }) public plugins?: any[];
@property({ type: Number }) public height?: number;
@state() private _chartHeight?: number;
@state() private _tooltip?: Tooltip;
@state() private _hiddenDatasets: Set<number> = new Set();
protected firstUpdated() {
this._setupChart();
this.data.datasets.forEach((dataset, index) => {
if (dataset.hidden) {
this._hiddenDatasets.add(index);
}
});
}
public willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (!this.hasUpdated || !this.chart) {
return;
}
if (changedProps.has("plugins")) {
this.chart.destroy();
this._setupChart();
return;
}
if (changedProps.has("chartType")) {
this.chart.config.type = this.chartType;
}
if (changedProps.has("data")) {
this.chart.data = this.data;
}
if (changedProps.has("options")) {
this.chart.options = this._createOptions();
}
this.chart.update("none");
}
protected render() {
return html`
${this.options?.plugins?.legend?.display === true
? html`<div class="chartLegend">
<ul>
${this.data.datasets.map(
(dataset, index) => html`<li
.datasetIndex=${index}
@click=${this._legendClick}
class=${classMap({
hidden: this._hiddenDatasets.has(index),
})}
>
<div
class="bullet"
style=${styleMap({
backgroundColor: dataset.backgroundColor as string,
borderColor: dataset.borderColor as string,
})}
></div>
${dataset.label}
</li>`
)}
</ul>
</div>`
: ""}
<div
class="chartContainer"
style=${styleMap({
height: `${this.height ?? this._chartHeight}px`,
overflow: this._chartHeight ? "initial" : "hidden",
})}
>
<canvas></canvas>
${this._tooltip
? html`<div
class="chartTooltip ${classMap({ [this._tooltip.yAlign]: true })}"
style=${styleMap({
top: this._tooltip.top,
left: this._tooltip.left,
})}
>
<div class="title">${this._tooltip.title}</div>
${this._tooltip.beforeBody
? html`<div class="beforeBody">
${this._tooltip.beforeBody}
</div>`
: ""}
<div>
<ul>
${this._tooltip.body.map(
(item, i) => html`<li>
<div
class="bullet"
style=${styleMap({
backgroundColor: this._tooltip!.labelColors[i]
.backgroundColor as string,
borderColor: this._tooltip!.labelColors[i]
.borderColor as string,
})}
></div>
${item.lines.join("\n")}
</li>`
)}
</ul>
</div>
${this._tooltip.footer.length
? html`<div class="footer">
${this._tooltip.footer.map((item) => html`${item}<br />`)}
</div>`
: ""}
</div>`
: ""}
</div>
`;
}
private async _setupChart() {
const ctx: CanvasRenderingContext2D = this.renderRoot
.querySelector("canvas")!
.getContext("2d")!;
const ChartConstructor = (await import("../../resources/chartjs")).Chart;
const computedStyles = getComputedStyle(this);
ChartConstructor.defaults.borderColor =
computedStyles.getPropertyValue("--divider-color");
ChartConstructor.defaults.color = computedStyles.getPropertyValue(
"--secondary-text-color"
);
this.chart = new ChartConstructor(ctx, {
type: this.chartType,
data: this.data,
options: this._createOptions(),
plugins: this._createPlugins(),
});
}
private _createOptions() {
return {
...this.options,
plugins: {
...this.options?.plugins,
tooltip: {
...this.options?.plugins?.tooltip,
enabled: false,
external: (context) => this._handleTooltip(context),
},
legend: {
...this.options?.plugins?.legend,
display: false,
},
},
};
}
private _createPlugins() {
return [
...(this.plugins || []),
{
id: "afterRenderHook",
afterRender: (chart) => {
this._chartHeight = chart.height;
},
legend: {
...this.options?.plugins?.legend,
display: false,
},
},
];
}
private _legendClick(ev) {
if (!this.chart) {
return;
}
const index = ev.currentTarget.datasetIndex;
if (this.chart.isDatasetVisible(index)) {
this.chart.setDatasetVisibility(index, false);
this._hiddenDatasets.add(index);
} else {
this.chart.setDatasetVisibility(index, true);
this._hiddenDatasets.delete(index);
}
this.chart.update("none");
this.requestUpdate("_hiddenDatasets");
}
private _handleTooltip(context: {
chart: Chart;
tooltip: TooltipModel<any>;
}) {
if (context.tooltip.opacity === 0) {
this._tooltip = undefined;
return;
}
this._tooltip = {
...context.tooltip,
top: this.chart!.canvas.offsetTop + context.tooltip.caretY + 12 + "px",
left:
this.chart!.canvas.offsetLeft +
clamp(context.tooltip.caretX, 100, this.clientWidth - 100) -
100 +
"px",
};
}
public updateChart = (): void => {
if (this.chart) {
this.chart.update();
}
};
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
}
.chartContainer {
overflow: hidden;
height: 0;
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
canvas {
max-height: var(--chart-max-height, 400px);
}
.chartLegend {
text-align: center;
}
.chartLegend li {
cursor: pointer;
display: inline-flex;
padding: 0 8px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
box-sizing: border-box;
align-items: center;
color: var(--secondary-text-color);
}
.chartLegend .hidden {
text-decoration: line-through;
}
.chartLegend .bullet,
.chartTooltip .bullet {
border-width: 1px;
border-style: solid;
border-radius: 50%;
display: inline-block;
height: 16px;
margin-right: 6px;
width: 16px;
flex-shrink: 0;
box-sizing: border-box;
}
.chartTooltip .bullet {
align-self: baseline;
}
:host([rtl]) .chartLegend .bullet,
:host([rtl]) .chartTooltip .bullet {
margin-right: inherit;
margin-left: 6px;
}
.chartTooltip {
padding: 8px;
font-size: 90%;
position: absolute;
background: rgba(80, 80, 80, 0.9);
color: white;
border-radius: 4px;
pointer-events: none;
z-index: 1000;
width: 200px;
box-sizing: border-box;
}
:host([rtl]) .chartTooltip {
direction: rtl;
}
.chartLegend ul,
.chartTooltip ul {
display: inline-block;
padding: 0 0px;
margin: 8px 0 0 0;
width: 100%;
}
.chartTooltip ul {
margin: 0 4px;
}
.chartTooltip li {
display: flex;
white-space: pre-line;
align-items: center;
line-height: 16px;
padding: 4px 0;
}
.chartTooltip .title {
text-align: center;
font-weight: 500;
}
.chartTooltip .footer {
font-weight: 500;
}
.chartTooltip .beforeBody {
text-align: center;
font-weight: 300;
word-break: break-all;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-chart-base": HaChartBase;
}
}

View File

@@ -1,400 +0,0 @@
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { html, LitElement, PropertyValues } from "lit";
import { property, state } from "lit/decorators";
import { getColorByIndex } from "../../common/color/colors";
import {
formatNumber,
numberFormatToLocale,
} from "../../common/string/format_number";
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() public names: boolean | Record<string, string> = false;
@property() public unit?: string;
@property() public identifier?: string;
@property({ type: Boolean }) public isSingleDevice = false;
@property({ attribute: false }) public endTime?: Date;
@state() private _chartData?: ChartData<"line">;
@state() private _chartOptions?: ChartOptions<"line">;
protected render() {
return html`
<ha-chart-base
.data=${this._chartData}
.options=${this._chartOptions}
chart-type="line"
></ha-chart-base>
`;
}
public willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
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,
},
title: {
display: true,
text: this.unit,
},
},
},
plugins: {
tooltip: {
mode: "nearest",
callbacks: {
label: (context) =>
`${context.dataset.label}: ${formatNumber(
context.parsed.y,
this.hass.locale
)} ${this.unit}`,
},
},
filler: {
propagate: true,
},
legend: {
display: !this.isSingleDevice,
labels: {
usePointStyle: true,
},
},
},
hover: {
mode: "nearest",
},
elements: {
line: {
tension: 0.1,
borderWidth: 1.5,
},
point: {
hitRadius: 5,
},
},
// @ts-expect-error
locale: numberFormatToLocale(this.hass.locale),
};
}
if (changedProps.has("data")) {
this._generateData();
}
}
private _generateData() {
let colorIndex = 0;
const computedStyles = getComputedStyle(this);
const entityStates = this.data;
const datasets: ChartDataset<"line">[] = [];
let endTime: Date;
if (entityStates.length === 0) {
return;
}
endTime =
this.endTime ||
// Get the highest date from the last date of each device
new Date(
Math.max(
...entityStates.map((devSts) =>
new Date(
devSts.states[devSts.states.length - 1].last_changed
).getTime()
)
)
);
if (endTime > new Date()) {
endTime = new Date();
}
const names = this.names || {};
entityStates.forEach((states) => {
const domain = states.domain;
const name = names[states.entity_id] || states.name;
// array containing [value1, value2, etc]
let prevValues: any[] | null = null;
const data: ChartDataset<"line">[] = [];
const pushData = (timestamp: Date, datavalues: any[] | 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;
}
data.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++;
}
data.push({
label: nameY,
fill: fill ? "origin" : false,
borderColor: color,
backgroundColor: color + "7F",
stepped: step ? "before" : false,
pointRadius: 0,
data: [],
});
};
if (
domain === "thermostat" ||
domain === "climate" ||
domain === "water_heater"
) {
const hasHvacAction = states.states.some(
(entityState) => entityState.attributes?.hvac_action
);
const isHeating =
domain === "climate" && hasHvacAction
? (entityState: LineChartState) =>
entityState.attributes?.hvac_action === "heating"
: (entityState: LineChartState) => entityState.state === "heat";
const isCooling =
domain === "climate" && hasHvacAction
? (entityState: LineChartState) =>
entityState.attributes?.hvac_action === "cooling"
: (entityState: LineChartState) => entityState.state === "cool";
const hasHeat = states.states.some(isHeating);
const hasCool = states.states.some(isCooling);
// We differentiate between thermostats that have a target temperature
// range versus ones that have just a target temperature
// Using step chart by step-before so manually interpolation not needed.
const hasTargetRange = states.states.some(
(entityState) =>
entityState.attributes &&
entityState.attributes.target_temp_high !==
entityState.attributes.target_temp_low
);
addDataSet(
`${this.hass.localize("ui.card.climate.current_temperature", {
name: name,
})}`,
true
);
if (hasHeat) {
addDataSet(
`${this.hass.localize("ui.card.climate.heating", { name: name })}`,
true,
true,
computedStyles.getPropertyValue("--state-climate-heat-color")
);
// The "heating" series uses steppedArea to shade the area below the current
// temperature when the thermostat is calling for heat.
}
if (hasCool) {
addDataSet(
`${this.hass.localize("ui.card.climate.cooling", { name: name })}`,
true,
true,
computedStyles.getPropertyValue("--state-climate-cool-color")
);
// The "cooling" series uses steppedArea to shade the area below the current
// temperature when the thermostat is calling for heat.
}
if (hasTargetRange) {
addDataSet(
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
name: name,
mode: this.hass.localize("ui.card.climate.high"),
})}`,
true
);
addDataSet(
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
name: name,
mode: this.hass.localize("ui.card.climate.low"),
})}`,
true
);
} else {
addDataSet(
`${this.hass.localize("ui.card.climate.target_temperature_entity", {
name: name,
})}`,
true
);
}
states.states.forEach((entityState) => {
if (!entityState.attributes) return;
const curTemp = safeParseFloat(
entityState.attributes.current_temperature
);
const series = [curTemp];
if (hasHeat) {
series.push(isHeating(entityState) ? curTemp : null);
}
if (hasCool) {
series.push(isCooling(entityState) ? curTemp : null);
}
if (hasTargetRange) {
const targetHigh = safeParseFloat(
entityState.attributes.target_temp_high
);
const targetLow = safeParseFloat(
entityState.attributes.target_temp_low
);
series.push(targetHigh, targetLow);
pushData(new Date(entityState.last_changed), series);
} else {
const target = safeParseFloat(entityState.attributes.temperature);
series.push(target);
pushData(new Date(entityState.last_changed), series);
}
});
} else if (domain === "humidifier") {
addDataSet(
`${this.hass.localize("ui.card.humidifier.target_humidity_entity", {
name: name,
})}`,
true
);
addDataSet(
`${this.hass.localize("ui.card.humidifier.on_entity", {
name: name,
})}`,
true,
true
);
states.states.forEach((entityState) => {
if (!entityState.attributes) return;
const target = safeParseFloat(entityState.attributes.humidity);
const series = [target];
series.push(entityState.state === "on" ? target : null);
pushData(new Date(entityState.last_changed), series);
});
} else {
// Only disable interpolation for sensors
const isStep = domain === "sensor";
addDataSet(name, isStep);
let lastValue: number;
let lastDate: Date;
let lastNullDate: Date | null = null;
// Process chart data.
// When state is `unknown`, calculate the value and break the line.
states.states.forEach((entityState) => {
const value = safeParseFloat(entityState.state);
const date = new Date(entityState.last_changed);
if (value !== null && lastNullDate) {
const dateTime = date.getTime();
const lastNullDateTime = lastNullDate.getTime();
const lastDateTime = lastDate?.getTime();
const tmpValue =
(value - lastValue) *
((lastNullDateTime - lastDateTime) /
(dateTime - lastDateTime)) +
lastValue;
pushData(lastNullDate, [tmpValue]);
pushData(new Date(lastNullDateTime + 1), [null]);
pushData(date, [value]);
lastDate = date;
lastValue = value;
lastNullDate = null;
} else if (value !== null && lastNullDate === null) {
pushData(date, [value]);
lastDate = date;
lastValue = value;
} else if (
value === null &&
lastNullDate === null &&
lastValue !== undefined
) {
lastNullDate = date;
}
});
}
// Add an entry for final values
pushData(endTime, prevValues);
// Concat two arrays
Array.prototype.push.apply(datasets, data);
});
this._chartData = {
datasets,
};
}
}
customElements.define("state-history-chart-line", StateHistoryChartLine);
declare global {
interface HTMLElementTagNameMap {
"state-history-chart-line": StateHistoryChartLine;
}
}

View File

@@ -1,322 +0,0 @@
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { getColorByIndex } from "../../common/color/colors";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import { computeDomain } from "../../common/entity/compute_domain";
import { numberFormatToLocale } from "../../common/string/format_number";
import { computeRTL } from "../../common/util/compute_rtl";
import { TimelineEntity } from "../../data/history";
import { HomeAssistant } from "../../types";
import "./ha-chart-base";
import type { TimeLineData } from "./timeline-chart/const";
/** Binary sensor device classes for which the static colors for on/off need to be inverted.
* List the ones were "off" = good or normal state = should be rendered "green".
*/
const BINARY_SENSOR_DEVICE_CLASS_COLOR_INVERTED = new Set([
"battery",
"door",
"garage_door",
"gas",
"lock",
"opening",
"problem",
"safety",
"smoke",
"window",
]);
const STATIC_STATE_COLORS = new Set([
"on",
"off",
"home",
"not_home",
"unavailable",
"unknown",
"idle",
]);
const stateColorMap: Map<string, string> = new Map();
let colorIndex = 0;
const invertOnOff = (entityState?: HassEntity) =>
entityState &&
computeDomain(entityState.entity_id) === "binary_sensor" &&
"device_class" in entityState.attributes &&
BINARY_SENSOR_DEVICE_CLASS_COLOR_INVERTED.has(
entityState.attributes.device_class!
);
const getColor = (
stateString: string,
entityState: HassEntity,
computedStyles: CSSStyleDeclaration
) => {
if (invertOnOff(entityState)) {
stateString = stateString === "on" ? "off" : "on";
}
if (stateColorMap.has(stateString)) {
return stateColorMap.get(stateString);
}
if (STATIC_STATE_COLORS.has(stateString)) {
const color = computedStyles.getPropertyValue(
`--state-${stateString}-color`
);
stateColorMap.set(stateString, color);
return color;
}
const color = getColorByIndex(colorIndex);
colorIndex++;
stateColorMap.set(stateString, color);
return color;
};
@customElement("state-history-chart-timeline")
export class StateHistoryChartTimeline extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public data: TimelineEntity[] = [];
@property() public names: boolean | Record<string, string> = false;
@property() public unit?: string;
@property() public identifier?: string;
@property({ type: Boolean }) public isSingleDevice = false;
@property({ attribute: false }) public endTime?: Date;
@state() private _chartData?: ChartData<"timeline">;
@state() private _chartOptions?: ChartOptions<"timeline">;
protected render() {
return html`
<ha-chart-base
.data=${this._chartData}
.options=${this._chartOptions}
.height=${this.data.length * 30 + 30}
chart-type="timeline"
></ha-chart-base>
`;
}
public willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
this._chartOptions = {
maintainAspectRatio: false,
parsing: false,
animation: false,
scales: {
x: {
type: "timeline",
position: "bottom",
adapters: {
date: {
locale: this.hass.locale,
},
},
ticks: {
autoSkip: true,
maxRotation: 0,
sampleSize: 5,
autoSkipPadding: 20,
major: {
enabled: true,
},
font: (context) =>
context.tick && context.tick.major
? ({ weight: "bold" } as any)
: {},
},
grid: {
offset: false,
},
time: {
tooltipFormat: "datetimeseconds",
},
},
y: {
type: "category",
barThickness: 20,
offset: true,
grid: {
display: false,
drawBorder: false,
drawTicks: false,
},
ticks: {
display: this.data.length !== 1,
},
afterSetDimensions: (y) => {
y.maxWidth = y.chart.width * 0.18;
},
position: computeRTL(this.hass) ? "right" : "left",
},
},
plugins: {
tooltip: {
mode: "nearest",
callbacks: {
title: (context) =>
context![0].chart!.data!.labels![
context[0].datasetIndex
] as string,
beforeBody: (context) => context[0].dataset.label || "",
label: (item) => {
const d = item.dataset.data[item.dataIndex] as TimeLineData;
return [
d.label || "",
formatDateTimeWithSeconds(d.start, this.hass.locale),
formatDateTimeWithSeconds(d.end, this.hass.locale),
];
},
labelColor: (item) => ({
borderColor: (item.dataset.data[item.dataIndex] as TimeLineData)
.color!,
backgroundColor: (
item.dataset.data[item.dataIndex] as TimeLineData
).color!,
}),
},
},
filler: {
propagate: true,
},
},
// @ts-expect-error
locale: numberFormatToLocale(this.hass.locale),
};
}
if (changedProps.has("data")) {
this._generateData();
}
}
private _generateData() {
const computedStyles = getComputedStyle(this);
let stateHistory = this.data;
if (!stateHistory) {
stateHistory = [];
}
const startTime = new Date(
stateHistory.reduce(
(minTime, stateInfo) =>
Math.min(minTime, new Date(stateInfo.data[0].last_changed).getTime()),
new Date().getTime()
)
);
// end time is Math.max(startTime, last_event)
let endTime =
this.endTime ||
new Date(
stateHistory.reduce(
(maxTime, stateInfo) =>
Math.max(
maxTime,
new Date(
stateInfo.data[stateInfo.data.length - 1].last_changed
).getTime()
),
startTime.getTime()
)
);
if (endTime > new Date()) {
endTime = new Date();
}
const labels: string[] = [];
const datasets: ChartDataset<"timeline">[] = [];
const names = this.names || {};
// stateHistory is a list of lists of sorted state objects
stateHistory.forEach((stateInfo) => {
let newLastChanged: Date;
let prevState: string | null = null;
let locState: string | null = null;
let prevLastChanged = startTime;
const entityDisplay: string =
names[stateInfo.entity_id] || stateInfo.name;
const dataRow: TimeLineData[] = [];
stateInfo.data.forEach((entityState) => {
let newState: string | null = entityState.state;
const timeStamp = new Date(entityState.last_changed);
if (!newState) {
newState = null;
}
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;
}
if (prevState === null) {
prevState = newState;
locState = entityState.state_localize;
prevLastChanged = new Date(entityState.last_changed);
} else if (newState !== prevState) {
newLastChanged = new Date(entityState.last_changed);
dataRow.push({
start: prevLastChanged,
end: newLastChanged,
label: locState,
color: getColor(
prevState,
this.hass.states[stateInfo.entity_id],
computedStyles
),
});
prevState = newState;
locState = entityState.state_localize;
prevLastChanged = newLastChanged;
}
});
if (prevState !== null) {
dataRow.push({
start: prevLastChanged,
end: endTime,
label: locState,
color: getColor(
prevState,
this.hass.states[stateInfo.entity_id],
computedStyles
),
});
}
datasets.push({
data: dataRow,
label: stateInfo.entity_id,
});
labels.push(entityDisplay);
});
this._chartData = {
labels: labels,
datasets: datasets,
};
}
static get styles(): CSSResultGroup {
return css`
ha-chart-base {
--chart-max-height: none;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"state-history-chart-timeline": StateHistoryChartTimeline;
}
}

View File

@@ -1,402 +0,0 @@
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 {
formatNumber,
numberFormatToLocale,
} from "../../common/string/format_number";
import {
getStatisticIds,
Statistics,
statisticsHaveType,
StatisticsMetaData,
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({ type: Array }) public statisticIds?: StatisticsMetaData[];
@property() public names: boolean | Record<string, string> = false;
@property() public unit?: string;
@property({ attribute: false }) public endTime?: Date;
@property({ type: Array }) public statTypes: Array<StatisticType> = [
"sum",
"min",
"mean",
"max",
];
@property() public chartType: ChartType = "line";
@property({ type: Boolean }) public isLoadingData = false;
@state() private _chartData: ChartData = { datasets: [] };
@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") || changedProps.has("statTypes")) {
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: "datetime",
},
},
y: {
beginAtZero: false,
ticks: {
maxTicksLimit: 7,
},
title: {
display: this.unit,
text: this.unit,
},
},
},
plugins: {
tooltip: {
mode: "nearest",
callbacks: {
label: (context) =>
`${context.dataset.label}: ${formatNumber(
context.parsed.y,
this.hass.locale
)} ${
// @ts-ignore
context.dataset.unit || ""
}`,
},
},
filler: {
propagate: true,
},
legend: {
display: true,
labels: {
usePointStyle: true,
},
},
},
hover: {
mode: "nearest",
},
elements: {
line: {
tension: 0.4,
borderWidth: 1.5,
},
bar: { borderWidth: 1.5, borderRadius: 4 },
point: {
hitRadius: 5,
},
},
// @ts-expect-error
locale: numberFormatToLocale(this.hass.locale),
};
}
private async _getStatisticIds() {
this.statisticIds = await getStatisticIds(this.hass);
}
private async _generateData() {
if (!this.statisticsData) {
return;
}
if (!this.statisticIds) {
await this._getStatisticIds();
}
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();
}
let unit: string | undefined | null;
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;
}
}
const meta = this.statisticIds!.find(
(stat) => stat.statistic_id === firstStat.statistic_id
);
if (!this.unit) {
if (unit === undefined) {
unit = meta?.unit_of_measurement;
} else if (unit !== meta?.unit_of_measurement) {
unit = null;
}
}
// 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 color = getColorByIndex(colorIndex);
colorIndex++;
const statTypes: this["statTypes"] = [];
const drawBands =
this.statTypes.includes("mean") && statisticsHaveType(stats, "mean");
const sortedTypes = drawBands
? [...this.statTypes].sort((a, b) => {
if (a === "min" || b === "max") {
return -1;
}
if (a === "max" || b === "min") {
return +1;
}
return 0;
})
: this.statTypes;
sortedTypes.forEach((type) => {
if (statisticsHaveType(stats, type)) {
const band = drawBands && (type === "min" || type === "max");
statTypes.push(type);
statDataSets.push({
label: `${name} (${this.hass.localize(
`ui.components.statistics_charts.statistic_types.${type}`
)})
`,
fill: drawBands
? type === "min"
? "+1"
: type === "max"
? "-1"
: false
: false,
borderColor: band ? color + "7F" : color,
backgroundColor: band ? color + "3F" : color + "7F",
pointRadius: 0,
data: [],
// @ts-ignore
unit: meta?.unit_of_measurement,
band,
});
}
});
let prevDate: Date | null = null;
// Process chart data.
let initVal: number | null = null;
let prevSum: number | null = null;
stats.forEach((stat) => {
const date = new Date(stat.start);
if (prevDate === date) {
return;
}
prevDate = date;
const dataValues: Array<number | null> = [];
statTypes.forEach((type) => {
let val: number | null;
if (type === "sum") {
if (!initVal) {
initVal = val = stat.state;
prevSum = stat.sum;
} else {
val = initVal + ((stat.sum || 0) - prevSum!);
}
} else {
val = stat[type];
}
dataValues.push(val !== null ? Math.round(val * 100) / 100 : null);
});
pushData(date, dataValues);
});
// Add an entry for final values
pushData(endTime, prevValues);
// Concat two arrays
Array.prototype.push.apply(totalDataSets, statDataSets);
});
if (unit !== null) {
this._chartOptions = {
...this._chartOptions,
scales: {
...this._chartOptions!.scales,
y: {
...(this._chartOptions!.scales!.y as Record<string, unknown>),
title: { display: unit, text: unit },
},
},
};
}
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

@@ -1,18 +0,0 @@
export interface TimeLineData {
start: Date;
end: Date;
label?: string | null;
color?: string;
}
declare module "chart.js" {
interface ChartTypeRegistry {
timeline: {
chartOptions: BarControllerChartOptions;
datasetOptions: BarControllerDatasetOptions;
defaultDataPoint: TimeLineData;
parsedDataType: any;
scales: "timeline";
};
}
}

View File

@@ -1,59 +0,0 @@
import { BarElement, BarOptions, BarProps } from "chart.js";
import { hex2rgb } from "../../../common/color/convert-color";
import { luminosity } from "../../../common/color/rgb";
export interface TextBarProps extends BarProps {
text?: string | null;
options?: Partial<TextBaroptions>;
}
export interface TextBaroptions extends BarOptions {
textPad?: number;
textColor?: string;
backgroundColor: string;
}
export class TextBarElement extends BarElement {
static id = "textbar";
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"]);
if (!text) {
return;
}
ctx.beginPath();
const textRect = ctx.measureText(text);
if (
textRect.width === 0 ||
textRect.width + (options.textPad || 4) + 2 > width
) {
return;
}
const textColor =
options.textColor ||
(options.backgroundColor &&
(luminosity(hex2rgb(options.backgroundColor)) > 0.5 ? "#000" : "#fff"));
// ctx.font = "12px arial";
ctx.fillStyle = textColor;
ctx.lineWidth = 0;
ctx.strokeStyle = textColor;
ctx.textBaseline = "middle";
ctx.fillText(
text,
x - width / 2 + (options.textPad || 4),
y + (base - y) / 2
);
}
tooltipPosition(useFinalPosition: boolean) {
const { x, y, base } = this.getProps(["x", "y", "base"], useFinalPosition);
return { x, y: y + (base - y) / 2 };
}
}

View File

@@ -1,160 +0,0 @@
import { BarController, BarElement } from "chart.js";
import { TimeLineData } from "./const";
import { TextBarProps } from "./textbar-element";
function parseValue(entry, item, vScale, i) {
const startValue = vScale.parse(entry.start, i);
const endValue = vScale.parse(entry.end, i);
const min = Math.min(startValue, endValue);
const max = Math.max(startValue, endValue);
let barStart = min;
let barEnd = max;
if (Math.abs(min) > Math.abs(max)) {
barStart = max;
barEnd = min;
}
// Store `barEnd` (furthest away from origin) as parsed value,
// to make stacking straight forward
item[vScale.axis] = barEnd;
item._custom = {
barStart,
barEnd,
start: startValue,
end: endValue,
min,
max,
};
return item;
}
export class TimelineController extends BarController {
static id = "timeline";
static defaults = {
dataElementType: "textbar",
dataElementOptions: ["text", "textColor", "textPadding"],
elements: {
showText: true,
textPadding: 4,
minBarWidth: 1,
},
layout: {
padding: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
},
};
static overrides = {
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
};
parseObjectData(meta, data, start, count) {
const iScale = meta.iScale;
const vScale = meta.vScale;
const labels = iScale.getLabels();
const singleScale = iScale === vScale;
const parsed: any[] = [];
let i;
let ilen;
let item;
let entry;
for (i = start, ilen = start + count; i < ilen; ++i) {
entry = data[i];
item = {};
item[iScale.axis] = singleScale || iScale.parse(labels[i], i);
parsed.push(parseValue(entry, item, vScale, i));
}
return parsed;
}
getLabelAndValue(index) {
const meta = this._cachedMeta;
const { vScale } = meta;
const data = this.getDataset().data[index] as TimeLineData;
return {
label: vScale!.getLabelForValue(this.index) || "",
value: data.label || "",
};
}
updateElements(
bars: BarElement[],
start: number,
count: number,
mode: "reset" | "resize" | "none" | "hide" | "show" | "normal" | "active"
) {
const vScale = this._cachedMeta.vScale!;
const iScale = this._cachedMeta.iScale!;
const dataset = this.getDataset();
const firstOpts = this.resolveDataElementOptions(start, mode);
const sharedOptions = this.getSharedOptions(firstOpts);
const includeOptions = this.includeOptions(mode, sharedOptions!);
const horizontal = vScale.isHorizontal();
this.updateSharedOptions(sharedOptions!, mode, firstOpts);
for (let index = start; index < start + count; index++) {
const data = dataset.data[index] as TimeLineData;
// @ts-ignore
const y = vScale.getPixelForValue(this.index);
// @ts-ignore
const xStart = iScale.getPixelForValue(data.start.getTime());
// @ts-ignore
const xEnd = iScale.getPixelForValue(data.end.getTime());
const width = xEnd - xStart;
const height = 10;
const properties: TextBarProps = {
horizontal,
x: xStart + width / 2, // Center of the bar
y: y - height, // Top of bar
width,
height: 0,
base: y + height, // Bottom of bar,
// Text
text: data.label,
};
if (includeOptions) {
properties.options =
sharedOptions || this.resolveDataElementOptions(index, mode);
properties.options = {
...properties.options,
backgroundColor: data.color,
};
}
this.updateElement(bars[index], index, properties as any, mode);
}
}
removeHoverStyle(_element, _datasetIndex, _index) {
// this._setStyle(element, index, 'active', false);
}
setHoverStyle(_element, _datasetIndex, _index) {
// this._setStyle(element, index, 'active', true);
}
}

View File

@@ -1,55 +0,0 @@
import { TimeScale } from "chart.js";
import { TimeLineData } from "./const";
export class TimeLineScale extends TimeScale {
static id = "timeline";
static defaults = {
position: "bottom",
tooltips: {
mode: "nearest",
},
ticks: {
autoSkip: true,
},
};
determineDataLimits() {
const options = this.options;
// @ts-ignore
const adapter = this._adapter;
const unit = options.time.unit || "day";
let { min, max } = this.getUserBounds();
const chart = this.chart;
// Convert data to timestamps
chart.data.datasets.forEach((dataset, index) => {
if (!chart.isDatasetVisible(index)) {
return;
}
for (const data of dataset.data as TimeLineData[]) {
let timestamp0 = adapter.parse(data.start, this);
let timestamp1 = adapter.parse(data.end, this);
if (timestamp0 > timestamp1) {
[timestamp0, timestamp1] = [timestamp1, timestamp0];
}
if (min > timestamp0 && timestamp0) {
min = timestamp0;
}
if (max < timestamp1 && timestamp1) {
max = timestamp1;
}
}
});
// In case there is no valid min/max, var's use today limits
min =
isFinite(min) && !isNaN(min) ? min : +adapter.startOf(Date.now(), unit);
max = isFinite(max) && !isNaN(max) ? max : +adapter.endOf(Date.now(), unit);
// Make sure that max is strictly higher than min (required by the lookup table)
this.min = Math.min(min, max - 1);
this.max = Math.max(min + 1, max);
}
}

View File

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

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