mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-07 04:06:31 +00:00
Compare commits
1 Commits
20210803.1
...
checkbox-s
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0bfeb22209 |
@@ -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/*"]
|
||||||
}
|
}
|
||||||
|
76
.github/workflows/ci.yaml
vendored
76
.github/workflows/ci.yaml
vendored
@@ -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:
|
||||||
|
22
.github/workflows/demo.yaml
vendored
22
.github/workflows/demo.yaml
vendored
@@ -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:
|
||||||
|
11
.github/workflows/release.yaml
vendored
11
.github/workflows/release.yaml
vendored
@@ -7,8 +7,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
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 +29,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
|
||||||
|
42
.github/workflows/translations.yaml
vendored
42
.github/workflows/translations.yaml
vendored
@@ -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
10
.gitignore
vendored
@@ -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
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -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);
|
|
||||||
}
|
|
@@ -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
8
.yarn/plugins/@yarnpkg/plugin-typescript.cjs
vendored
8
.yarn/plugins/@yarnpkg/plugin-typescript.cjs
vendored
File diff suppressed because one or more lines are too long
55
.yarn/releases/yarn-2.4.2.cjs
vendored
55
.yarn/releases/yarn-2.4.2.cjs
vendored
File diff suppressed because one or more lines are too long
@@ -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
|
|
@@ -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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
@@ -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
|
||||||
@@ -57,23 +58,12 @@ module.exports.babelOptions = ({ 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,14 +76,8 @@ module.exports.babelOptions = ({ latestBuild }) => ({
|
|||||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||||
["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],
|
["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],
|
||||||
["@babel/plugin-proposal-private-methods", { loose: true }],
|
["@babel/plugin-proposal-private-methods", { loose: true }],
|
||||||
["@babel/plugin-proposal-private-property-in-object", { loose: true }],
|
|
||||||
["@babel/plugin-proposal-class-properties", { loose: true }],
|
["@babel/plugin-proposal-class-properties", { loose: true }],
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
exclude: [
|
|
||||||
// \\ for Windows, / for Mac OS and Linux
|
|
||||||
/node_modules[\\/]core-js/,
|
|
||||||
/node_modules[\\/]webpack[\\/]buildin/,
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const outputPath = (outputRoot, latestBuild) =>
|
const outputPath = (outputRoot, latestBuild) =>
|
||||||
|
@@ -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",
|
||||||
|
@@ -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" }
|
||||||
|
@@ -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"),
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -19,12 +19,10 @@ const bothBuilds = (createConfigFunc, params) => [
|
|||||||
createConfigFunc({ ...params, latestBuild: false }),
|
createConfigFunc({ ...params, latestBuild: false }),
|
||||||
];
|
];
|
||||||
|
|
||||||
const isWsl =
|
const isWsl = fs
|
||||||
fs.existsSync("/proc/version") &&
|
.readFileSync("/proc/version", "utf-8")
|
||||||
fs
|
.toLocaleLowerCase()
|
||||||
.readFileSync("/proc/version", "utf-8")
|
.includes("microsoft");
|
||||||
.toLocaleLowerCase()
|
|
||||||
.includes("microsoft");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{
|
* @param {{
|
||||||
@@ -86,15 +84,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/, poll: isWsl },
|
||||||
? bothBuilds(createAppConfig, { isProdBuild: false })
|
|
||||||
: createAppConfig({ isProdBuild: false, latestBuild: true })
|
|
||||||
).watch(
|
|
||||||
{
|
|
||||||
ignored: /build-translations/,
|
|
||||||
poll: isWsl,
|
|
||||||
},
|
|
||||||
doneHandler()
|
doneHandler()
|
||||||
);
|
);
|
||||||
gulp.watch(
|
gulp.watch(
|
||||||
|
@@ -49,16 +49,12 @@ const createWebpackConfig = ({
|
|||||||
test: /\.m?js$|\.ts$/,
|
test: /\.m?js$|\.ts$/,
|
||||||
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 +66,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 +112,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 +134,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
|
||||||
|
@@ -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
|
||||||
>.
|
>.
|
||||||
|
@@ -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: [
|
||||||
{
|
{
|
||||||
|
@@ -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: {
|
||||||
|
@@ -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());
|
||||||
};
|
};
|
||||||
|
@@ -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",
|
||||||
},
|
},
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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";
|
||||||
|
@@ -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]) => {
|
||||||
|
@@ -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",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
|
@@ -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: [] }));
|
|
||||||
};
|
|
@@ -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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
@@ -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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
@@ -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;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@@ -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());
|
||||||
|
@@ -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 },
|
||||||
|
@@ -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];
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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,
|
||||||
@@ -986,6 +977,7 @@ class HassioAddonInfo extends LitElement {
|
|||||||
showDialogSupervisorUpdate(this, {
|
showDialogSupervisorUpdate(this, {
|
||||||
supervisor: this.supervisor,
|
supervisor: this.supervisor,
|
||||||
name: this.addon.name,
|
name: this.addon.name,
|
||||||
|
slug: this.addon.slug,
|
||||||
version: this.addon.version_latest,
|
version: this.addon.version_latest,
|
||||||
snapshotParams: {
|
snapshotParams: {
|
||||||
name: `addon_${this.addon.slug}_${this.addon.version}`,
|
name: `addon_${this.addon.slug}_${this.addon.version}`,
|
||||||
|
@@ -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";
|
||||||
|
@@ -86,7 +86,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",
|
||||||
@@ -161,6 +161,7 @@ export class HassioUpdate extends LitElement {
|
|||||||
showDialogSupervisorUpdate(this, {
|
showDialogSupervisorUpdate(this, {
|
||||||
supervisor: this.supervisor,
|
supervisor: this.supervisor,
|
||||||
name: "Home Assistant Core",
|
name: "Home Assistant Core",
|
||||||
|
slug: "core",
|
||||||
version: this.supervisor.core.version_latest,
|
version: this.supervisor.core.version_latest,
|
||||||
snapshotParams: {
|
snapshotParams: {
|
||||||
name: `core_${this.supervisor.core.version}`,
|
name: `core_${this.supervisor.core.version}`,
|
||||||
|
@@ -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%;
|
||||||
|
@@ -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";
|
||||||
|
@@ -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()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
|
@@ -30,8 +30,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;
|
||||||
@@ -298,7 +297,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",
|
||||||
});
|
});
|
||||||
|
@@ -2,19 +2,32 @@ import "@material/mwc-button/mwc-button";
|
|||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, state } from "lit/decorators";
|
import { customElement, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
|
import "../../../../src/components/ha-checkbox";
|
||||||
import "../../../../src/components/ha-circular-progress";
|
import "../../../../src/components/ha-circular-progress";
|
||||||
import "../../../../src/components/ha-dialog";
|
import "../../../../src/components/ha-dialog";
|
||||||
import "../../../../src/components/ha-settings-row";
|
import "../../../../src/components/ha-settings-row";
|
||||||
import "../../../../src/components/ha-svg-icon";
|
import "../../../../src/components/ha-svg-icon";
|
||||||
import "../../../../src/components/ha-switch";
|
|
||||||
import {
|
import {
|
||||||
extractApiErrorMessage,
|
extractApiErrorMessage,
|
||||||
ignoreSupervisorError,
|
ignoreSupervisorError,
|
||||||
} from "../../../../src/data/hassio/common";
|
} from "../../../../src/data/hassio/common";
|
||||||
|
import {
|
||||||
|
SupervisorFrontendPrefrences,
|
||||||
|
fetchSupervisorFrontendPreferences,
|
||||||
|
saveSupervisorFrontendPreferences,
|
||||||
|
} from "../../../../src/data/supervisor/supervisor";
|
||||||
import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot";
|
import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot";
|
||||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../src/types";
|
import type { HomeAssistant } from "../../../../src/types";
|
||||||
import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update";
|
import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
|
||||||
|
const snapshot_before_update = memoizeOne(
|
||||||
|
(slug: string, frontendPrefrences: SupervisorFrontendPrefrences) =>
|
||||||
|
slug in frontendPrefrences.snapshot_before_update
|
||||||
|
? frontendPrefrences.snapshot_before_update[slug]
|
||||||
|
: true
|
||||||
|
);
|
||||||
|
|
||||||
@customElement("dialog-supervisor-update")
|
@customElement("dialog-supervisor-update")
|
||||||
class DialogSupervisorUpdate extends LitElement {
|
class DialogSupervisorUpdate extends LitElement {
|
||||||
@@ -22,12 +35,12 @@ class DialogSupervisorUpdate extends LitElement {
|
|||||||
|
|
||||||
@state() private _opened = false;
|
@state() private _opened = false;
|
||||||
|
|
||||||
@state() private _createSnapshot = true;
|
|
||||||
|
|
||||||
@state() private _action: "snapshot" | "update" | null = null;
|
@state() private _action: "snapshot" | "update" | null = null;
|
||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
@state() private _frontendPrefrences?: SupervisorFrontendPrefrences;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private _dialogParams?: SupervisorDialogSupervisorUpdateParams;
|
private _dialogParams?: SupervisorDialogSupervisorUpdateParams;
|
||||||
|
|
||||||
@@ -36,27 +49,30 @@ class DialogSupervisorUpdate extends LitElement {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this._opened = true;
|
this._opened = true;
|
||||||
this._dialogParams = params;
|
this._dialogParams = params;
|
||||||
|
this._frontendPrefrences = await fetchSupervisorFrontendPreferences(
|
||||||
|
this.hass
|
||||||
|
);
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): void {
|
public closeDialog(): void {
|
||||||
this._action = null;
|
this._action = null;
|
||||||
this._createSnapshot = true;
|
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
this._dialogParams = undefined;
|
this._dialogParams = undefined;
|
||||||
|
this._frontendPrefrences = undefined;
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this._dialogParams) {
|
if (!this._dialogParams || !this._frontendPrefrences) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
@@ -82,6 +98,16 @@ class DialogSupervisorUpdate extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ha-settings-row>
|
<ha-settings-row>
|
||||||
|
<ha-checkbox
|
||||||
|
.checked=${snapshot_before_update(
|
||||||
|
this._dialogParams.slug,
|
||||||
|
this._frontendPrefrences
|
||||||
|
)}
|
||||||
|
haptic
|
||||||
|
@click=${this._toggleSnapshot}
|
||||||
|
slot="prefix"
|
||||||
|
>
|
||||||
|
</ha-checkbox>
|
||||||
<span slot="heading">
|
<span slot="heading">
|
||||||
${this._dialogParams.supervisor.localize(
|
${this._dialogParams.supervisor.localize(
|
||||||
"dialog.update.snapshot"
|
"dialog.update.snapshot"
|
||||||
@@ -94,12 +120,6 @@ class DialogSupervisorUpdate extends LitElement {
|
|||||||
this._dialogParams.name
|
this._dialogParams.name
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<ha-switch
|
|
||||||
.checked=${this._createSnapshot}
|
|
||||||
haptic
|
|
||||||
@click=${this._toggleSnapshot}
|
|
||||||
>
|
|
||||||
</ha-switch>
|
|
||||||
</ha-settings-row>
|
</ha-settings-row>
|
||||||
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
|
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
|
||||||
${this._dialogParams.supervisor.localize("common.cancel")}
|
${this._dialogParams.supervisor.localize("common.cancel")}
|
||||||
@@ -133,12 +153,27 @@ class DialogSupervisorUpdate extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _toggleSnapshot() {
|
private async _toggleSnapshot(): Promise<void> {
|
||||||
this._createSnapshot = !this._createSnapshot;
|
this._frontendPrefrences!.snapshot_before_update[
|
||||||
|
this._dialogParams!.slug
|
||||||
|
] = !snapshot_before_update(
|
||||||
|
this._dialogParams!.slug,
|
||||||
|
this._frontendPrefrences!
|
||||||
|
);
|
||||||
|
|
||||||
|
await saveSupervisorFrontendPreferences(
|
||||||
|
this.hass,
|
||||||
|
this._frontendPrefrences!
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _update() {
|
private async _update() {
|
||||||
if (this._createSnapshot) {
|
if (
|
||||||
|
snapshot_before_update(
|
||||||
|
this._dialogParams!.slug,
|
||||||
|
this._frontendPrefrences!
|
||||||
|
)
|
||||||
|
) {
|
||||||
this._action = "snapshot";
|
this._action = "snapshot";
|
||||||
try {
|
try {
|
||||||
await createHassioPartialSnapshot(
|
await createHassioPartialSnapshot(
|
||||||
@@ -158,8 +193,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,6 +5,7 @@ export interface SupervisorDialogSupervisorUpdateParams {
|
|||||||
supervisor: Supervisor;
|
supervisor: Supervisor;
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
|
slug: string;
|
||||||
snapshotParams: any;
|
snapshotParams: any;
|
||||||
updateHandler: () => Promise<void>;
|
updateHandler: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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: {
|
||||||
|
@@ -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 })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -164,6 +164,7 @@ class HassioCoreInfo extends LitElement {
|
|||||||
showDialogSupervisorUpdate(this, {
|
showDialogSupervisorUpdate(this, {
|
||||||
supervisor: this.supervisor,
|
supervisor: this.supervisor,
|
||||||
name: "Home Assistant Core",
|
name: "Home Assistant Core",
|
||||||
|
slug: "core",
|
||||||
version: this.supervisor.core.version_latest,
|
version: this.supervisor.core.version_latest,
|
||||||
snapshotParams: {
|
snapshotParams: {
|
||||||
name: `core_${this.supervisor.core.version}`,
|
name: `core_${this.supervisor.core.version}`,
|
||||||
|
@@ -113,7 +113,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 +190,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>`
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
"*.{js,ts}": 'eslint --ignore-pattern "**/build-scripts/**/*.js" --fix',
|
"*.{js,ts}": "eslint --fix",
|
||||||
"!(/translations)*.{js,ts,json,css,md,html}": "prettier --write",
|
"!(/translations)*.{js,ts,json,css,md,html}": "prettier --write",
|
||||||
};
|
};
|
||||||
|
126
package.json
126
package.json
@@ -42,72 +42,72 @@
|
|||||||
"@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-input": "^3.0.1",
|
"@polymer/iron-input": "^3.0.1",
|
||||||
"@polymer/iron-overlay-behavior": "^3.0.3",
|
"@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.5",
|
||||||
"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 +122,7 @@
|
|||||||
"proxy-polyfill": "^0.3.1",
|
"proxy-polyfill": "^0.3.1",
|
||||||
"punycode": "^2.1.1",
|
"punycode": "^2.1.1",
|
||||||
"qrcode": "^1.4.4",
|
"qrcode": "^1.4.4",
|
||||||
"regenerator-runtime": "^0.13.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 +144,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 +171,22 @@
|
|||||||
"@types/mocha": "^8.2.2",
|
"@types/mocha": "^8.2.2",
|
||||||
"@types/sortablejs": "^1.10.6",
|
"@types/sortablejs": "^1.10.6",
|
||||||
"@types/webspeechapi": "^0.0.29",
|
"@types/webspeechapi": "^0.0.29",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.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 +198,7 @@
|
|||||||
"gulp-zopfli-green": "^3.0.1",
|
"gulp-zopfli-green": "^3.0.1",
|
||||||
"html-minifier": "^4.0.0",
|
"html-minifier": "^4.0.0",
|
||||||
"husky": "^1.3.1",
|
"husky": "^1.3.1",
|
||||||
"lint-staged": "^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 +207,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 +218,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"
|
||||||
},
|
},
|
||||||
|
22
script/core
22
script/core
@@ -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"
|
||||||
|
4
setup.py
4
setup.py
@@ -2,9 +2,9 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="home-assistant-frontend",
|
name="home-assistant-frontend",
|
||||||
version="20210803.1",
|
version="20210603.0",
|
||||||
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",
|
||||||
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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];
|
|
||||||
}
|
|
@@ -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++) {
|
||||||
|
@@ -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. */
|
||||||
|
@@ -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();
|
|
||||||
|
@@ -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, {
|
||||||
|
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@@ -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];
|
||||||
},
|
}
|
||||||
});
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
@@ -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") {
|
||||||
|
@@ -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";
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -1,2 +0,0 @@
|
|||||||
export const clamp = (value: number, min: number, max: number) =>
|
|
||||||
Math.min(Math.max(value, min), max);
|
|
@@ -1,2 +0,0 @@
|
|||||||
export const round = (value: number, precision = 2): number =>
|
|
||||||
Math.round(value * 10 ** precision) / 10 ** precision;
|
|
@@ -67,11 +67,9 @@ class SearchInput extends LitElement {
|
|||||||
changedProps.has("noUnderline") &&
|
changedProps.has("noUnderline") &&
|
||||||
(this.noUnderline || changedProps.get("noUnderline") !== undefined)
|
(this.noUnderline || changedProps.get("noUnderline") !== undefined)
|
||||||
) {
|
) {
|
||||||
(
|
(this._input.inputElement!.parentElement!.shadowRoot!.querySelector(
|
||||||
this._input.inputElement!.parentElement!.shadowRoot!.querySelector(
|
"div.unfocused-line"
|
||||||
"div.unfocused-line"
|
) as HTMLElement).style.display = this.noUnderline ? "none" : "block";
|
||||||
) as HTMLElement
|
|
||||||
).style.display = this.noUnderline ? "none" : "block";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
|
@@ -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);
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
@@ -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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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 };
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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;
|
|
||||||
};
|
|
@@ -1,4 +1,4 @@
|
|||||||
import { Layout1d, scroll } from "@lit-labs/virtualizer";
|
import { Layout1d, scroll } from "../../resources/lit-virtualizer";
|
||||||
import deepClone from "deep-clone-simple";
|
import deepClone from "deep-clone-simple";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
@@ -10,10 +10,10 @@ import {
|
|||||||
} from "lit";
|
} from "lit";
|
||||||
import {
|
import {
|
||||||
customElement,
|
customElement,
|
||||||
eventOptions,
|
|
||||||
property,
|
property,
|
||||||
query,
|
|
||||||
state,
|
state,
|
||||||
|
query,
|
||||||
|
eventOptions,
|
||||||
} from "lit/decorators";
|
} from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
@@ -360,8 +360,9 @@ export class HaDataTable extends LitElement {
|
|||||||
.rowId=${row[this.id]}
|
.rowId=${row[this.id]}
|
||||||
@click=${this._handleRowClick}
|
@click=${this._handleRowClick}
|
||||||
class="mdc-data-table__row ${classMap({
|
class="mdc-data-table__row ${classMap({
|
||||||
"mdc-data-table__row--selected":
|
"mdc-data-table__row--selected": this._checkedRows.includes(
|
||||||
this._checkedRows.includes(String(row[this.id])),
|
String(row[this.id])
|
||||||
|
),
|
||||||
clickable: this.clickable,
|
clickable: this.clickable,
|
||||||
})}"
|
})}"
|
||||||
aria-selected=${ifDefined(
|
aria-selected=${ifDefined(
|
||||||
@@ -405,15 +406,17 @@ export class HaDataTable extends LitElement {
|
|||||||
"mdc-data-table__cell--icon": Boolean(
|
"mdc-data-table__cell--icon": Boolean(
|
||||||
column.type === "icon"
|
column.type === "icon"
|
||||||
),
|
),
|
||||||
"mdc-data-table__cell--icon-button":
|
"mdc-data-table__cell--icon-button": Boolean(
|
||||||
Boolean(column.type === "icon-button"),
|
column.type === "icon-button"
|
||||||
|
),
|
||||||
grows: Boolean(column.grows),
|
grows: Boolean(column.grows),
|
||||||
forceLTR: Boolean(column.forceLTR),
|
forceLTR: Boolean(column.forceLTR),
|
||||||
})}"
|
})}"
|
||||||
style=${column.width
|
style=${column.width
|
||||||
? styleMap({
|
? styleMap({
|
||||||
[column.grows ? "minWidth" : "width"]:
|
[column.grows
|
||||||
column.width,
|
? "minWidth"
|
||||||
|
: "width"]: column.width,
|
||||||
maxWidth: column.maxWidth
|
maxWidth: column.maxWidth
|
||||||
? column.maxWidth
|
? column.maxWidth
|
||||||
: "",
|
: "",
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import "@material/mwc-button/mwc-button";
|
import "@material/mwc-button/mwc-button";
|
||||||
import "@material/mwc-icon-button/mwc-icon-button";
|
import "@material/mwc-icon-button/mwc-icon-button";
|
||||||
import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
|
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
|
||||||
import "@polymer/paper-input/paper-input";
|
import "@polymer/paper-input/paper-input";
|
||||||
import "@polymer/paper-item/paper-item";
|
import "@polymer/paper-item/paper-item";
|
||||||
import "@polymer/paper-item/paper-item-body";
|
import "@polymer/paper-item/paper-item-body";
|
||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
@@ -39,6 +38,7 @@ import { PolymerChangedEvent } from "../../polymer-types";
|
|||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import "../ha-svg-icon";
|
import "../ha-svg-icon";
|
||||||
import "./ha-devices-picker";
|
import "./ha-devices-picker";
|
||||||
|
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
|
||||||
|
|
||||||
interface DevicesByArea {
|
interface DevicesByArea {
|
||||||
[areaId: string]: AreaDevices;
|
[areaId: string]: AreaDevices;
|
||||||
@@ -52,27 +52,20 @@ interface AreaDevices {
|
|||||||
|
|
||||||
const rowRenderer: ComboBoxLitRenderer<AreaDevices> = (item) => html`<style>
|
const rowRenderer: ComboBoxLitRenderer<AreaDevices> = (item) => html`<style>
|
||||||
paper-item {
|
paper-item {
|
||||||
|
width: 100%;
|
||||||
|
margin: -10px 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: -10px;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
}
|
||||||
#content {
|
mwc-icon-button {
|
||||||
display: flex;
|
float: right;
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
ha-svg-icon {
|
.devices {
|
||||||
padding-left: 2px;
|
|
||||||
margin-right: -2px;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
:host(:not([selected])) ha-svg-icon {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
:host([selected]) paper-item {
|
.devices.visible {
|
||||||
margin-left: 10px;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
|
|
||||||
<paper-item>
|
<paper-item>
|
||||||
<paper-item-body two-line="">
|
<paper-item-body two-line="">
|
||||||
<div class="name">${item.name}</div>
|
<div class="name">${item.name}</div>
|
||||||
|
@@ -11,8 +11,6 @@ import {
|
|||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state, query } from "lit/decorators";
|
import { customElement, property, state, query } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
|
|
||||||
import { mdiCheck } from "@mdi/js";
|
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { computeDomain } from "../../common/entity/compute_domain";
|
import { computeDomain } from "../../common/entity/compute_domain";
|
||||||
import { compare } from "../../common/string/compare";
|
import { compare } from "../../common/string/compare";
|
||||||
@@ -35,6 +33,7 @@ import { PolymerChangedEvent } from "../../polymer-types";
|
|||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import "../ha-combo-box";
|
import "../ha-combo-box";
|
||||||
import type { HaComboBox } from "../ha-combo-box";
|
import type { HaComboBox } from "../ha-combo-box";
|
||||||
|
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
|
||||||
|
|
||||||
interface Device {
|
interface Device {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -48,27 +47,10 @@ export type HaDevicePickerDeviceFilterFunc = (
|
|||||||
|
|
||||||
const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<style>
|
const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<style>
|
||||||
paper-item {
|
paper-item {
|
||||||
|
margin: -10px 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: -10px;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
#content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
ha-svg-icon {
|
|
||||||
padding-left: 2px;
|
|
||||||
margin-right: -2px;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
:host(:not([selected])) ha-svg-icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
:host([selected]) paper-item {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
|
|
||||||
<paper-item>
|
<paper-item>
|
||||||
<paper-item-body two-line>
|
<paper-item-body two-line>
|
||||||
${item.name}
|
${item.name}
|
||||||
|
661
src/components/entity/ha-chart-base.js
Normal file
661
src/components/entity/ha-chart-base.js
Normal file
@@ -0,0 +1,661 @@
|
|||||||
|
/* eslint-plugin-disable lit */
|
||||||
|
import { IronResizableBehavior } from "@polymer/iron-resizable-behavior/iron-resizable-behavior";
|
||||||
|
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
|
||||||
|
import { timeOut } from "@polymer/polymer/lib/utils/async";
|
||||||
|
import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
|
||||||
|
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||||
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
|
import { formatTime } from "../../common/datetime/format_time";
|
||||||
|
import "../ha-icon-button";
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
/* global Chart moment Color */
|
||||||
|
|
||||||
|
let scriptsLoaded = null;
|
||||||
|
|
||||||
|
class HaChartBase extends mixinBehaviors(
|
||||||
|
[IronResizableBehavior],
|
||||||
|
PolymerElement
|
||||||
|
) {
|
||||||
|
static get template() {
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.chartHeader {
|
||||||
|
padding: 6px 0 0 0;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.chartHeader > div {
|
||||||
|
vertical-align: top;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
.chartHeader > div.chartTitle {
|
||||||
|
padding-top: 8px;
|
||||||
|
flex: 0 0 0;
|
||||||
|
max-width: 30%;
|
||||||
|
}
|
||||||
|
.chartHeader > div.chartLegend {
|
||||||
|
flex: 1 1;
|
||||||
|
min-width: 70%;
|
||||||
|
}
|
||||||
|
:root {
|
||||||
|
user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
}
|
||||||
|
.chartTooltip {
|
||||||
|
font-size: 90%;
|
||||||
|
opacity: 1;
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(80, 80, 80, 0.9);
|
||||||
|
color: white;
|
||||||
|
border-radius: 3px;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translate(-50%, 12px);
|
||||||
|
z-index: 1000;
|
||||||
|
width: 200px;
|
||||||
|
transition: opacity 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
:host([rtl]) .chartTooltip {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
.chartLegend ul,
|
||||||
|
.chartTooltip ul {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 0px;
|
||||||
|
margin: 5px 0 0 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.chartTooltip ul {
|
||||||
|
margin: 0 3px;
|
||||||
|
}
|
||||||
|
.chartTooltip li {
|
||||||
|
display: block;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
.chartTooltip li::first-line {
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
.chartTooltip .title {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.chartTooltip .beforeBody {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 300;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.chartLegend li {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 6px;
|
||||||
|
max-width: 49%;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.chartLegend li:nth-child(odd):last-of-type {
|
||||||
|
/* Make last item take full width if it is odd-numbered. */
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.chartLegend li[data-hidden] {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
.chartLegend em,
|
||||||
|
.chartTooltip em {
|
||||||
|
border-radius: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
height: 10px;
|
||||||
|
margin-right: 4px;
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
:host([rtl]) .chartTooltip em {
|
||||||
|
margin-right: inherit;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
ha-icon-button {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<template is="dom-if" if="[[unit]]">
|
||||||
|
<div class="chartHeader">
|
||||||
|
<div class="chartTitle">[[unit]]</div>
|
||||||
|
<div class="chartLegend">
|
||||||
|
<ul>
|
||||||
|
<template is="dom-repeat" items="[[metas]]">
|
||||||
|
<li on-click="_legendClick" data-hidden$="[[item.hidden]]">
|
||||||
|
<em style$="background-color:[[item.bgColor]]"></em>
|
||||||
|
[[item.label]]
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div id="chartTarget" style="height:40px; width:100%">
|
||||||
|
<canvas id="chartCanvas"></canvas>
|
||||||
|
<div
|
||||||
|
class$="chartTooltip [[tooltip.yAlign]]"
|
||||||
|
style$="opacity:[[tooltip.opacity]]; top:[[tooltip.top]]; left:[[tooltip.left]]; padding:[[tooltip.yPadding]]px [[tooltip.xPadding]]px"
|
||||||
|
>
|
||||||
|
<div class="title">[[tooltip.title]]</div>
|
||||||
|
<template is="dom-if" if="[[tooltip.beforeBody]]">
|
||||||
|
<div class="beforeBody">[[tooltip.beforeBody]]</div>
|
||||||
|
</template>
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
<template is="dom-repeat" items="[[tooltip.lines]]">
|
||||||
|
<li>
|
||||||
|
<em style$="background-color:[[item.bgColor]]"></em
|
||||||
|
>[[item.text]]
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get chart() {
|
||||||
|
return this._chart;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
data: Object,
|
||||||
|
identifier: String,
|
||||||
|
rendered: {
|
||||||
|
type: Boolean,
|
||||||
|
notify: true,
|
||||||
|
value: false,
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
metas: {
|
||||||
|
type: Array,
|
||||||
|
value: () => [],
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
type: Object,
|
||||||
|
value: () => ({
|
||||||
|
opacity: "0",
|
||||||
|
left: "0",
|
||||||
|
top: "0",
|
||||||
|
xPadding: "5",
|
||||||
|
yPadding: "3",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
unit: Object,
|
||||||
|
rtl: {
|
||||||
|
type: Boolean,
|
||||||
|
reflectToAttribute: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static get observers() {
|
||||||
|
return ["onPropsChange(data)"];
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._isAttached = true;
|
||||||
|
this.onPropsChange();
|
||||||
|
this._resizeListener = () => {
|
||||||
|
this._debouncer = Debouncer.debounce(
|
||||||
|
this._debouncer,
|
||||||
|
timeOut.after(10),
|
||||||
|
() => {
|
||||||
|
if (this._isAttached) {
|
||||||
|
this.resizeChart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof ResizeObserver === "function") {
|
||||||
|
this.resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
entries.forEach(() => {
|
||||||
|
this._resizeListener();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.resizeObserver.observe(this.$.chartTarget);
|
||||||
|
} else {
|
||||||
|
this.addEventListener("iron-resize", this._resizeListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scriptsLoaded === null) {
|
||||||
|
scriptsLoaded = import("../../resources/ha-chart-scripts.js");
|
||||||
|
}
|
||||||
|
scriptsLoaded.then((ChartModule) => {
|
||||||
|
this.ChartClass = ChartModule.default;
|
||||||
|
this.onPropsChange();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._isAttached = false;
|
||||||
|
if (this.resizeObserver) {
|
||||||
|
this.resizeObserver.unobserve(this.$.chartTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.removeEventListener("iron-resize", this._resizeListener);
|
||||||
|
|
||||||
|
if (this._resizeTimer !== undefined) {
|
||||||
|
clearInterval(this._resizeTimer);
|
||||||
|
this._resizeTimer = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPropsChange() {
|
||||||
|
if (!this._isAttached || !this.ChartClass || !this.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.drawChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
_customTooltips(tooltip) {
|
||||||
|
// Hide if no tooltip
|
||||||
|
if (tooltip.opacity === 0) {
|
||||||
|
this.set(["tooltip", "opacity"], 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Set caret Position
|
||||||
|
if (tooltip.yAlign) {
|
||||||
|
this.set(["tooltip", "yAlign"], tooltip.yAlign);
|
||||||
|
} else {
|
||||||
|
this.set(["tooltip", "yAlign"], "no-transform");
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = tooltip.title ? tooltip.title[0] || "" : "";
|
||||||
|
this.set(["tooltip", "title"], title);
|
||||||
|
|
||||||
|
if (tooltip.beforeBody) {
|
||||||
|
this.set(["tooltip", "beforeBody"], tooltip.beforeBody.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyLines = tooltip.body.map((n) => n.lines);
|
||||||
|
|
||||||
|
// Set Text
|
||||||
|
if (tooltip.body) {
|
||||||
|
this.set(
|
||||||
|
["tooltip", "lines"],
|
||||||
|
bodyLines.map((body, i) => {
|
||||||
|
const colors = tooltip.labelColors[i];
|
||||||
|
return {
|
||||||
|
color: colors.borderColor,
|
||||||
|
bgColor: colors.backgroundColor,
|
||||||
|
text: body.join("\n"),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const parentWidth = this.$.chartTarget.clientWidth;
|
||||||
|
let positionX = tooltip.caretX;
|
||||||
|
const positionY = this._chart.canvas.offsetTop + tooltip.caretY;
|
||||||
|
if (tooltip.caretX + 100 > parentWidth) {
|
||||||
|
positionX = parentWidth - 100;
|
||||||
|
} else if (tooltip.caretX < 100) {
|
||||||
|
positionX = 100;
|
||||||
|
}
|
||||||
|
positionX += this._chart.canvas.offsetLeft;
|
||||||
|
// Display, position, and set styles for font
|
||||||
|
this.tooltip = {
|
||||||
|
...this.tooltip,
|
||||||
|
opacity: 1,
|
||||||
|
left: `${positionX}px`,
|
||||||
|
top: `${positionY}px`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_legendClick(event) {
|
||||||
|
event = event || window.event;
|
||||||
|
event.stopPropagation();
|
||||||
|
let target = event.target || event.srcElement;
|
||||||
|
while (target.nodeName !== "LI") {
|
||||||
|
// user clicked child, find parent LI
|
||||||
|
target = target.parentElement;
|
||||||
|
}
|
||||||
|
const index = event.model.itemsIndex;
|
||||||
|
|
||||||
|
const meta = this._chart.getDatasetMeta(index);
|
||||||
|
meta.hidden =
|
||||||
|
meta.hidden === null ? !this._chart.data.datasets[index].hidden : null;
|
||||||
|
this.set(
|
||||||
|
["metas", index, "hidden"],
|
||||||
|
this._chart.isDatasetVisible(index) ? null : "hidden"
|
||||||
|
);
|
||||||
|
this._chart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
_drawLegend() {
|
||||||
|
const chart = this._chart;
|
||||||
|
// New data for old graph. Keep metadata.
|
||||||
|
const preserveVisibility =
|
||||||
|
this._oldIdentifier && this.identifier === this._oldIdentifier;
|
||||||
|
this._oldIdentifier = this.identifier;
|
||||||
|
this.set(
|
||||||
|
"metas",
|
||||||
|
this._chart.data.datasets.map((x, i) => ({
|
||||||
|
label: x.label,
|
||||||
|
color: x.color,
|
||||||
|
bgColor: x.backgroundColor,
|
||||||
|
hidden:
|
||||||
|
preserveVisibility && i < this.metas.length
|
||||||
|
? this.metas[i].hidden
|
||||||
|
: !chart.isDatasetVisible(i),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
let updateNeeded = false;
|
||||||
|
if (preserveVisibility) {
|
||||||
|
for (let i = 0; i < this.metas.length; i++) {
|
||||||
|
const meta = chart.getDatasetMeta(i);
|
||||||
|
if (!!meta.hidden !== !!this.metas[i].hidden) updateNeeded = true;
|
||||||
|
meta.hidden = this.metas[i].hidden ? true : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updateNeeded) {
|
||||||
|
chart.update();
|
||||||
|
}
|
||||||
|
this.unit = this.data.unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
_formatTickValue(value, index, values) {
|
||||||
|
if (values.length === 0) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
const date = new Date(values[index].value);
|
||||||
|
return formatTime(date, this.hass.locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawChart() {
|
||||||
|
const data = this.data.data;
|
||||||
|
const ctx = this.$.chartCanvas;
|
||||||
|
|
||||||
|
if ((!data.datasets || !data.datasets.length) && !this._chart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.data.type !== "timeline" && data.datasets.length > 0) {
|
||||||
|
const cnt = data.datasets.length;
|
||||||
|
const colors = this.constructor.getColorList(cnt);
|
||||||
|
for (let loopI = 0; loopI < cnt; loopI++) {
|
||||||
|
data.datasets[loopI].borderColor = colors[loopI].rgbString();
|
||||||
|
data.datasets[loopI].backgroundColor = colors[loopI]
|
||||||
|
.alpha(0.6)
|
||||||
|
.rgbaString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._chart) {
|
||||||
|
this._customTooltips({ opacity: 0 });
|
||||||
|
this._chart.data = data;
|
||||||
|
this._chart.update({ duration: 0 });
|
||||||
|
if (this.isTimeline) {
|
||||||
|
this._chart.options.scales.yAxes[0].gridLines.display = data.length > 1;
|
||||||
|
} else if (this.data.legend === true) {
|
||||||
|
this._drawLegend();
|
||||||
|
}
|
||||||
|
this.resizeChart();
|
||||||
|
} else {
|
||||||
|
if (!data.datasets) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._customTooltips({ opacity: 0 });
|
||||||
|
const plugins = [{ afterRender: () => this._setRendered(true) }];
|
||||||
|
let options = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
animation: {
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
animationDuration: 0,
|
||||||
|
},
|
||||||
|
responsiveAnimationDuration: 0,
|
||||||
|
tooltips: {
|
||||||
|
enabled: false,
|
||||||
|
custom: this._customTooltips.bind(this),
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
line: {
|
||||||
|
spanGaps: true,
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
font: "12px 'Roboto', 'sans-serif'",
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
fontFamily: "'Roboto', 'sans-serif'",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
options = Chart.helpers.merge(options, this.data.options);
|
||||||
|
options.scales.xAxes[0].ticks.callback = this._formatTickValue.bind(this);
|
||||||
|
if (this.data.type === "timeline") {
|
||||||
|
this.set("isTimeline", true);
|
||||||
|
if (this.data.colors !== undefined) {
|
||||||
|
this._colorFunc = this.constructor.getColorGenerator(
|
||||||
|
this.data.colors.staticColors,
|
||||||
|
this.data.colors.staticColorIndex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this._colorFunc !== undefined) {
|
||||||
|
options.elements.colorFunction = this._colorFunc;
|
||||||
|
}
|
||||||
|
if (data.datasets.length === 1) {
|
||||||
|
if (options.scales.yAxes[0].ticks) {
|
||||||
|
options.scales.yAxes[0].ticks.display = false;
|
||||||
|
} else {
|
||||||
|
options.scales.yAxes[0].ticks = { display: false };
|
||||||
|
}
|
||||||
|
if (options.scales.yAxes[0].gridLines) {
|
||||||
|
options.scales.yAxes[0].gridLines.display = false;
|
||||||
|
} else {
|
||||||
|
options.scales.yAxes[0].gridLines = { display: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$.chartTarget.style.height = "50px";
|
||||||
|
} else {
|
||||||
|
this.$.chartTarget.style.height = "160px";
|
||||||
|
}
|
||||||
|
const chartData = {
|
||||||
|
type: this.data.type,
|
||||||
|
data: this.data.data,
|
||||||
|
options: options,
|
||||||
|
plugins: plugins,
|
||||||
|
};
|
||||||
|
// Async resize after dom update
|
||||||
|
this._chart = new this.ChartClass(ctx, chartData);
|
||||||
|
if (this.isTimeline !== true && this.data.legend === true) {
|
||||||
|
this._drawLegend();
|
||||||
|
}
|
||||||
|
this.resizeChart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeChart() {
|
||||||
|
if (!this._chart) return;
|
||||||
|
// Chart not ready
|
||||||
|
if (this._resizeTimer === undefined) {
|
||||||
|
this._resizeTimer = setInterval(this.resizeChart.bind(this), 10);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearInterval(this._resizeTimer);
|
||||||
|
this._resizeTimer = undefined;
|
||||||
|
|
||||||
|
this._resizeChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
_resizeChart() {
|
||||||
|
const chartTarget = this.$.chartTarget;
|
||||||
|
|
||||||
|
const options = this.data;
|
||||||
|
const data = options.data;
|
||||||
|
|
||||||
|
if (data.datasets.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isTimeline) {
|
||||||
|
this._chart.resize();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate chart height for Timeline chart
|
||||||
|
const areaTop = this._chart.chartArea.top;
|
||||||
|
const areaBot = this._chart.chartArea.bottom;
|
||||||
|
const height1 = this._chart.canvas.clientHeight;
|
||||||
|
if (areaBot > 0) {
|
||||||
|
this._axisHeight = height1 - areaBot + areaTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._axisHeight) {
|
||||||
|
chartTarget.style.height = "50px";
|
||||||
|
this._chart.resize();
|
||||||
|
this.resizeChart();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._axisHeight) {
|
||||||
|
const cnt = data.datasets.length;
|
||||||
|
const targetHeight = 30 * cnt + this._axisHeight + "px";
|
||||||
|
if (chartTarget.style.height !== targetHeight) {
|
||||||
|
chartTarget.style.height = targetHeight;
|
||||||
|
}
|
||||||
|
this._chart.resize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get HSL distributed color list
|
||||||
|
static getColorList(count) {
|
||||||
|
let processL = false;
|
||||||
|
if (count > 10) {
|
||||||
|
processL = true;
|
||||||
|
count = Math.ceil(count / 2);
|
||||||
|
}
|
||||||
|
const h1 = 360 / count;
|
||||||
|
const result = [];
|
||||||
|
for (let loopI = 0; loopI < count; loopI++) {
|
||||||
|
result[loopI] = Color().hsl(h1 * loopI, 80, 38);
|
||||||
|
if (processL) {
|
||||||
|
result[loopI + count] = Color().hsl(h1 * loopI, 80, 62);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getColorGenerator(staticColors, startIndex) {
|
||||||
|
// Known colors for static data,
|
||||||
|
// should add for very common state string manually.
|
||||||
|
// Palette modified from http://google.github.io/palette.js/ mpn65, Apache 2.0
|
||||||
|
const palette = [
|
||||||
|
"ff0029",
|
||||||
|
"66a61e",
|
||||||
|
"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",
|
||||||
|
];
|
||||||
|
function getColorIndex(idx) {
|
||||||
|
// Reuse the color if index too large.
|
||||||
|
return Color("#" + palette[idx % palette.length]);
|
||||||
|
}
|
||||||
|
const colorDict = {};
|
||||||
|
let colorIndex = 0;
|
||||||
|
if (startIndex > 0) colorIndex = startIndex;
|
||||||
|
if (staticColors) {
|
||||||
|
Object.keys(staticColors).forEach((c) => {
|
||||||
|
const c1 = staticColors[c];
|
||||||
|
if (isFinite(c1)) {
|
||||||
|
colorDict[c.toLowerCase()] = getColorIndex(c1);
|
||||||
|
} else {
|
||||||
|
colorDict[c.toLowerCase()] = Color(staticColors[c]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Custom color assign
|
||||||
|
function getColor(__, data) {
|
||||||
|
let ret;
|
||||||
|
const name = data[3];
|
||||||
|
if (name === null) return Color().hsl(0, 40, 38);
|
||||||
|
if (name === undefined) return Color().hsl(120, 40, 38);
|
||||||
|
let name1 = name.toLowerCase();
|
||||||
|
if (ret === undefined) {
|
||||||
|
if (data[4]) {
|
||||||
|
// Invert on/off if data[4] is true. Required for some binary_sensor device classes
|
||||||
|
// (BINARY_SENSOR_DEVICE_CLASS_COLOR_INVERTED) where "off" is the good (= green color) value.
|
||||||
|
name1 = name1 === "on" ? "off" : name1 === "off" ? "on" : name1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = colorDict[name1];
|
||||||
|
}
|
||||||
|
if (ret === undefined) {
|
||||||
|
ret = getColorIndex(colorIndex);
|
||||||
|
colorIndex++;
|
||||||
|
colorDict[name1] = ret;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
return getColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define("ha-chart-base", HaChartBase);
|
@@ -12,7 +12,7 @@ import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
|
|||||||
class HaEntitiesPickerLight extends LitElement {
|
class HaEntitiesPickerLight extends LitElement {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property({ type: Array }) public value?: string[];
|
@property() public value?: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show entities from specific domains.
|
* Show entities from specific domains.
|
||||||
@@ -30,22 +30,6 @@ class HaEntitiesPickerLight extends LitElement {
|
|||||||
@property({ type: Array, attribute: "exclude-domains" })
|
@property({ type: Array, attribute: "exclude-domains" })
|
||||||
public excludeDomains?: string[];
|
public excludeDomains?: string[];
|
||||||
|
|
||||||
/**
|
|
||||||
* Show only entities of these device classes.
|
|
||||||
* @type {Array}
|
|
||||||
* @attr include-device-classes
|
|
||||||
*/
|
|
||||||
@property({ type: Array, attribute: "include-device-classes" })
|
|
||||||
public includeDeviceClasses?: string[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show only entities with these unit of measuments.
|
|
||||||
* @type {Array}
|
|
||||||
* @attr include-unit-of-measurement
|
|
||||||
*/
|
|
||||||
@property({ type: Array, attribute: "include-unit-of-measurement" })
|
|
||||||
public includeUnitOfMeasurement?: string[];
|
|
||||||
|
|
||||||
@property({ attribute: "picked-entity-label" })
|
@property({ attribute: "picked-entity-label" })
|
||||||
public pickedEntityLabel?: string;
|
public pickedEntityLabel?: string;
|
||||||
|
|
||||||
@@ -67,8 +51,6 @@ class HaEntitiesPickerLight extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.includeDomains=${this.includeDomains}
|
.includeDomains=${this.includeDomains}
|
||||||
.excludeDomains=${this.excludeDomains}
|
.excludeDomains=${this.excludeDomains}
|
||||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
|
||||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
|
||||||
.entityFilter=${this._entityFilter}
|
.entityFilter=${this._entityFilter}
|
||||||
.value=${entityId}
|
.value=${entityId}
|
||||||
.label=${this.pickedEntityLabel}
|
.label=${this.pickedEntityLabel}
|
||||||
@@ -82,8 +64,6 @@ class HaEntitiesPickerLight extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.includeDomains=${this.includeDomains}
|
.includeDomains=${this.includeDomains}
|
||||||
.excludeDomains=${this.excludeDomains}
|
.excludeDomains=${this.excludeDomains}
|
||||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
|
||||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
|
||||||
.entityFilter=${this._entityFilter}
|
.entityFilter=${this._entityFilter}
|
||||||
.label=${this.pickEntityLabel}
|
.label=${this.pickEntityLabel}
|
||||||
@value-changed=${this._addEntity}
|
@value-changed=${this._addEntity}
|
||||||
@@ -101,11 +81,11 @@ class HaEntitiesPickerLight extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _updateEntities(entities) {
|
private async _updateEntities(entities) {
|
||||||
this.value = entities;
|
|
||||||
|
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: entities,
|
value: entities,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.value = entities;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _entityChanged(event: PolymerChangedEvent<string>) {
|
private _entityChanged(event: PolymerChangedEvent<string>) {
|
||||||
@@ -118,22 +98,20 @@ class HaEntitiesPickerLight extends LitElement {
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const currentEntities = this._currentEntities;
|
if (newValue === "") {
|
||||||
if (!newValue || currentEntities.includes(newValue)) {
|
this._updateEntities(
|
||||||
this._updateEntities(currentEntities.filter((ent) => ent !== curValue));
|
this._currentEntities.filter((ent) => ent !== curValue)
|
||||||
return;
|
);
|
||||||
|
} else {
|
||||||
|
this._updateEntities(
|
||||||
|
this._currentEntities.map((ent) => (ent === curValue ? newValue : ent))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this._updateEntities(
|
|
||||||
currentEntities.map((ent) => (ent === curValue ? newValue : ent))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _addEntity(event: PolymerChangedEvent<string>) {
|
private async _addEntity(event: PolymerChangedEvent<string>) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const toAdd = event.detail.value;
|
const toAdd = event.detail.value;
|
||||||
if (!toAdd) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
(event.currentTarget as any).value = "";
|
(event.currentTarget as any).value = "";
|
||||||
if (!toAdd) {
|
if (!toAdd) {
|
||||||
return;
|
return;
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import "@material/mwc-icon-button/mwc-icon-button";
|
import "@material/mwc-icon-button/mwc-icon-button";
|
||||||
import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
|
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
|
||||||
import "@polymer/paper-input/paper-input";
|
import "@polymer/paper-input/paper-input";
|
||||||
import "@polymer/paper-item/paper-item";
|
import "@polymer/paper-item/paper-item";
|
||||||
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
|
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
|
||||||
@@ -25,27 +25,10 @@ export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
|||||||
|
|
||||||
const rowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style>
|
const rowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style>
|
||||||
paper-item {
|
paper-item {
|
||||||
|
margin: -5px -10px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: -10px;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
#content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
ha-svg-icon {
|
|
||||||
padding-left: 2px;
|
|
||||||
margin-right: -2px;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
:host(:not([selected])) ha-svg-icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
:host([selected]) paper-item {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
|
|
||||||
<paper-item>${formatAttributeName(item)}</paper-item>`;
|
<paper-item>${formatAttributeName(item)}</paper-item>`;
|
||||||
|
|
||||||
@customElement("ha-entity-attribute-picker")
|
@customElement("ha-entity-attribute-picker")
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import "@material/mwc-icon-button/mwc-icon-button";
|
import "@material/mwc-icon-button/mwc-icon-button";
|
||||||
import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
|
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
|
||||||
import "@polymer/paper-input/paper-input";
|
import "@polymer/paper-input/paper-input";
|
||||||
import "@polymer/paper-item/paper-icon-item";
|
import "@polymer/paper-item/paper-icon-item";
|
||||||
import "@polymer/paper-item/paper-item-body";
|
import "@polymer/paper-item/paper-item-body";
|
||||||
@@ -28,25 +28,10 @@ export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
|||||||
|
|
||||||
const rowRenderer: ComboBoxLitRenderer<HassEntity> = (item) => html`<style>
|
const rowRenderer: ComboBoxLitRenderer<HassEntity> = (item) => html`<style>
|
||||||
paper-icon-item {
|
paper-icon-item {
|
||||||
|
margin: -10px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: -8px;
|
|
||||||
}
|
|
||||||
#content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
ha-svg-icon {
|
|
||||||
padding-left: 2px;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
:host(:not([selected])) ha-svg-icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
:host([selected]) paper-icon-item {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
|
|
||||||
<paper-icon-item>
|
<paper-icon-item>
|
||||||
<state-badge slot="item-icon" .stateObj=${item}></state-badge>
|
<state-badge slot="item-icon" .stateObj=${item}></state-badge>
|
||||||
<paper-item-body two-line="">
|
<paper-item-body two-line="">
|
||||||
@@ -57,8 +42,6 @@ const rowRenderer: ComboBoxLitRenderer<HassEntity> = (item) => html`<style>
|
|||||||
|
|
||||||
@customElement("ha-entity-picker")
|
@customElement("ha-entity-picker")
|
||||||
export class HaEntityPicker extends LitElement {
|
export class HaEntityPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public autofocus = false;
|
@property({ type: Boolean }) public autofocus = false;
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled?: boolean;
|
@property({ type: Boolean }) public disabled?: boolean;
|
||||||
@@ -66,6 +49,8 @@ export class HaEntityPicker extends LitElement {
|
|||||||
@property({ type: Boolean, attribute: "allow-custom-entity" })
|
@property({ type: Boolean, attribute: "allow-custom-entity" })
|
||||||
public allowCustomEntity;
|
public allowCustomEntity;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
@property() public value?: string;
|
@property() public value?: string;
|
||||||
@@ -94,14 +79,6 @@ export class HaEntityPicker extends LitElement {
|
|||||||
@property({ type: Array, attribute: "include-device-classes" })
|
@property({ type: Array, attribute: "include-device-classes" })
|
||||||
public includeDeviceClasses?: string[];
|
public includeDeviceClasses?: string[];
|
||||||
|
|
||||||
/**
|
|
||||||
* Show only entities with these unit of measuments.
|
|
||||||
* @type {Array}
|
|
||||||
* @attr include-unit-of-measurement
|
|
||||||
*/
|
|
||||||
@property({ type: Array, attribute: "include-unit-of-measurement" })
|
|
||||||
public includeUnitOfMeasurement?: string[];
|
|
||||||
|
|
||||||
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
|
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||||
|
|
||||||
@property({ type: Boolean }) public hideClearIcon = false;
|
@property({ type: Boolean }) public hideClearIcon = false;
|
||||||
@@ -133,8 +110,7 @@ export class HaEntityPicker extends LitElement {
|
|||||||
includeDomains: this["includeDomains"],
|
includeDomains: this["includeDomains"],
|
||||||
excludeDomains: this["excludeDomains"],
|
excludeDomains: this["excludeDomains"],
|
||||||
entityFilter: this["entityFilter"],
|
entityFilter: this["entityFilter"],
|
||||||
includeDeviceClasses: this["includeDeviceClasses"],
|
includeDeviceClasses: this["includeDeviceClasses"]
|
||||||
includeUnitOfMeasurement: this["includeUnitOfMeasurement"]
|
|
||||||
) => {
|
) => {
|
||||||
let states: HassEntity[] = [];
|
let states: HassEntity[] = [];
|
||||||
|
|
||||||
@@ -167,18 +143,6 @@ export class HaEntityPicker extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeUnitOfMeasurement) {
|
|
||||||
states = states.filter(
|
|
||||||
(stateObj) =>
|
|
||||||
// We always want to include the entity of the current value
|
|
||||||
stateObj.entity_id === this.value ||
|
|
||||||
(stateObj.attributes.unit_of_measurement &&
|
|
||||||
includeUnitOfMeasurement.includes(
|
|
||||||
stateObj.attributes.unit_of_measurement
|
|
||||||
))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entityFilter) {
|
if (entityFilter) {
|
||||||
states = states.filter(
|
states = states.filter(
|
||||||
(stateObj) =>
|
(stateObj) =>
|
||||||
@@ -220,7 +184,7 @@ export class HaEntityPicker extends LitElement {
|
|||||||
return !(!changedProps.has("_opened") && this._opened);
|
return !(!changedProps.has("_opened") && this._opened);
|
||||||
}
|
}
|
||||||
|
|
||||||
public willUpdate(changedProps: PropertyValues) {
|
protected updated(changedProps: PropertyValues) {
|
||||||
if (!this._initedStates || (changedProps.has("_opened") && this._opened)) {
|
if (!this._initedStates || (changedProps.has("_opened") && this._opened)) {
|
||||||
this._states = this._getStates(
|
this._states = this._getStates(
|
||||||
this._opened,
|
this._opened,
|
||||||
@@ -228,24 +192,23 @@ export class HaEntityPicker extends LitElement {
|
|||||||
this.includeDomains,
|
this.includeDomains,
|
||||||
this.excludeDomains,
|
this.excludeDomains,
|
||||||
this.entityFilter,
|
this.entityFilter,
|
||||||
this.includeDeviceClasses,
|
this.includeDeviceClasses
|
||||||
this.includeUnitOfMeasurement
|
|
||||||
);
|
);
|
||||||
if (this._initedStates) {
|
(this.comboBox as any).filteredItems = this._states;
|
||||||
(this.comboBox as any).filteredItems = this._states;
|
|
||||||
}
|
|
||||||
this._initedStates = true;
|
this._initedStates = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
|
if (!this.hass) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
return html`
|
return html`
|
||||||
<vaadin-combo-box-light
|
<vaadin-combo-box-light
|
||||||
item-value-path="entity_id"
|
item-value-path="entity_id"
|
||||||
item-label-path="entity_id"
|
item-label-path="entity_id"
|
||||||
.value=${this._value}
|
.value=${this._value}
|
||||||
.allowCustomValue=${this.allowCustomEntity}
|
.allowCustomValue=${this.allowCustomEntity}
|
||||||
.filteredItems=${this._states}
|
|
||||||
${comboBoxRenderer(rowRenderer)}
|
${comboBoxRenderer(rowRenderer)}
|
||||||
@opened-changed=${this._openedChanged}
|
@opened-changed=${this._openedChanged}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
|
@@ -1,289 +0,0 @@
|
|||||||
import "@material/mwc-icon-button/mwc-icon-button";
|
|
||||||
import { mdiCheck } from "@mdi/js";
|
|
||||||
import "@polymer/paper-input/paper-input";
|
|
||||||
import "@polymer/paper-item/paper-icon-item";
|
|
||||||
import "@polymer/paper-item/paper-item-body";
|
|
||||||
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
|
|
||||||
import { HassEntity } from "home-assistant-js-websocket";
|
|
||||||
import {
|
|
||||||
css,
|
|
||||||
CSSResultGroup,
|
|
||||||
html,
|
|
||||||
LitElement,
|
|
||||||
PropertyValues,
|
|
||||||
TemplateResult,
|
|
||||||
} from "lit";
|
|
||||||
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
|
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
|
||||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
|
||||||
import { compare } from "../../common/string/compare";
|
|
||||||
import { getStatisticIds, StatisticsMetaData } from "../../data/history";
|
|
||||||
import { PolymerChangedEvent } from "../../polymer-types";
|
|
||||||
import { HomeAssistant } from "../../types";
|
|
||||||
import { documentationUrl } from "../../util/documentation-url";
|
|
||||||
import "../ha-combo-box";
|
|
||||||
import type { HaComboBox } from "../ha-combo-box";
|
|
||||||
import "../ha-svg-icon";
|
|
||||||
import "./state-badge";
|
|
||||||
|
|
||||||
@customElement("ha-statistic-picker")
|
|
||||||
export class HaStatisticPicker extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property() public label?: string;
|
|
||||||
|
|
||||||
@property() public value?: string;
|
|
||||||
|
|
||||||
@property({ attribute: "statistic-types" })
|
|
||||||
public statisticTypes?: "mean" | "sum";
|
|
||||||
|
|
||||||
@property({ type: Array }) public statisticIds?: StatisticsMetaData[];
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show only statistics with these unit of measuments.
|
|
||||||
* @type {Array}
|
|
||||||
* @attr include-unit-of-measurement
|
|
||||||
*/
|
|
||||||
@property({ type: Array, attribute: "include-unit-of-measurement" })
|
|
||||||
public includeUnitOfMeasurement?: string[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show only statistics on entities.
|
|
||||||
* @type {Boolean}
|
|
||||||
* @attr entities-only
|
|
||||||
*/
|
|
||||||
@property({ type: Boolean, attribute: "entities-only" })
|
|
||||||
public entitiesOnly = false;
|
|
||||||
|
|
||||||
@state() private _opened?: boolean;
|
|
||||||
|
|
||||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
|
||||||
|
|
||||||
private _init = false;
|
|
||||||
|
|
||||||
private _rowRenderer: ComboBoxLitRenderer<{
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
state?: HassEntity;
|
|
||||||
}> = (item) => html`<style>
|
|
||||||
paper-icon-item {
|
|
||||||
padding: 0;
|
|
||||||
margin: -8px;
|
|
||||||
}
|
|
||||||
#content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
ha-svg-icon {
|
|
||||||
padding-left: 2px;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
:host(:not([selected])) ha-svg-icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
:host([selected]) paper-icon-item {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
|
|
||||||
<paper-icon-item>
|
|
||||||
<state-badge slot="item-icon" .stateObj=${item.state}></state-badge>
|
|
||||||
<paper-item-body two-line="">
|
|
||||||
${item.name}
|
|
||||||
<span secondary
|
|
||||||
>${item.id === "" || item.id === "__missing"
|
|
||||||
? html`<a
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
href="${documentationUrl(this.hass, "/more-info/statistics/")}"
|
|
||||||
>${this.hass.localize(
|
|
||||||
"ui.components.statistic-picker.learn_more"
|
|
||||||
)}</a
|
|
||||||
>`
|
|
||||||
: item.id}</span
|
|
||||||
>
|
|
||||||
</paper-item-body>
|
|
||||||
</paper-icon-item>`;
|
|
||||||
|
|
||||||
private _getStatistics = memoizeOne(
|
|
||||||
(
|
|
||||||
statisticIds: StatisticsMetaData[],
|
|
||||||
includeUnitOfMeasurement?: string[],
|
|
||||||
entitiesOnly?: boolean
|
|
||||||
): Array<{ id: string; name: string; state?: HassEntity }> => {
|
|
||||||
if (!statisticIds.length) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: "",
|
|
||||||
name: this.hass.localize(
|
|
||||||
"ui.components.statistic-picker.no_statistics"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeUnitOfMeasurement) {
|
|
||||||
statisticIds = statisticIds.filter((meta) =>
|
|
||||||
includeUnitOfMeasurement.includes(meta.unit_of_measurement)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const output: Array<{
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
state?: HassEntity;
|
|
||||||
}> = [];
|
|
||||||
statisticIds.forEach((meta) => {
|
|
||||||
const entityState = this.hass.states[meta.statistic_id];
|
|
||||||
if (!entityState) {
|
|
||||||
if (!entitiesOnly) {
|
|
||||||
output.push({ id: meta.statistic_id, name: meta.statistic_id });
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
output.push({
|
|
||||||
id: meta.statistic_id,
|
|
||||||
name: computeStateName(entityState),
|
|
||||||
state: entityState,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!output.length) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: "",
|
|
||||||
name: this.hass.localize("ui.components.statistic-picker.no_match"),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (output.length > 1) {
|
|
||||||
output.sort((a, b) => compare(a.name || "", b.name || ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
output.push({
|
|
||||||
id: "__missing",
|
|
||||||
name: this.hass.localize(
|
|
||||||
"ui.components.statistic-picker.missing_entity"
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
public open() {
|
|
||||||
this.comboBox?.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
public focus() {
|
|
||||||
this.comboBox?.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
public willUpdate(changedProps: PropertyValues) {
|
|
||||||
if (
|
|
||||||
(!this.hasUpdated && !this.statisticIds) ||
|
|
||||||
changedProps.has("statisticTypes")
|
|
||||||
) {
|
|
||||||
this._getStatisticIds();
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
(!this._init && this.statisticIds) ||
|
|
||||||
(changedProps.has("_opened") && this._opened)
|
|
||||||
) {
|
|
||||||
this._init = true;
|
|
||||||
if (this.hasUpdated) {
|
|
||||||
(this.comboBox as any).items = this._getStatistics(
|
|
||||||
this.statisticIds!,
|
|
||||||
this.includeUnitOfMeasurement,
|
|
||||||
this.entitiesOnly
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.updateComplete.then(() => {
|
|
||||||
(this.comboBox as any).items = this._getStatistics(
|
|
||||||
this.statisticIds!,
|
|
||||||
this.includeUnitOfMeasurement,
|
|
||||||
this.entitiesOnly
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
return html`
|
|
||||||
<ha-combo-box
|
|
||||||
.hass=${this.hass}
|
|
||||||
.label=${this.label === undefined && this.hass
|
|
||||||
? this.hass.localize("ui.components.statistic-picker.statistic")
|
|
||||||
: this.label}
|
|
||||||
.value=${this._value}
|
|
||||||
.renderer=${this._rowRenderer}
|
|
||||||
.disabled=${this.disabled}
|
|
||||||
item-value-path="id"
|
|
||||||
item-id-path="id"
|
|
||||||
item-label-path="name"
|
|
||||||
@opened-changed=${this._openedChanged}
|
|
||||||
@value-changed=${this._statisticChanged}
|
|
||||||
></ha-combo-box>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _getStatisticIds() {
|
|
||||||
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private get _value() {
|
|
||||||
return this.value || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private _statisticChanged(ev: PolymerChangedEvent<string>) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
let newValue = ev.detail.value;
|
|
||||||
if (newValue === "__missing") {
|
|
||||||
newValue = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newValue !== this._value) {
|
|
||||||
this._setValue(newValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
|
|
||||||
this._opened = ev.detail.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _setValue(value: string) {
|
|
||||||
this.value = value;
|
|
||||||
setTimeout(() => {
|
|
||||||
fireEvent(this, "value-changed", { value });
|
|
||||||
fireEvent(this, "change");
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return css`
|
|
||||||
paper-input > mwc-icon-button {
|
|
||||||
--mdc-icon-button-size: 24px;
|
|
||||||
padding: 2px;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
[hidden] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-statistic-picker": HaStatisticPicker;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,112 +0,0 @@
|
|||||||
import { html, LitElement, TemplateResult } from "lit";
|
|
||||||
import { customElement, property } from "lit/decorators";
|
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
|
||||||
import type { PolymerChangedEvent } from "../../polymer-types";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
|
||||||
import "./ha-statistic-picker";
|
|
||||||
|
|
||||||
@customElement("ha-statistics-picker")
|
|
||||||
class HaStatisticsPicker extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ type: Array }) public value?: string[];
|
|
||||||
|
|
||||||
@property({ type: Array }) public statisticIds?: string[];
|
|
||||||
|
|
||||||
@property({ attribute: "statistic-types" })
|
|
||||||
public statisticTypes?: "mean" | "sum";
|
|
||||||
|
|
||||||
@property({ attribute: "picked-statistic-label" })
|
|
||||||
public pickedStatisticLabel?: string;
|
|
||||||
|
|
||||||
@property({ attribute: "pick-statistic-label" })
|
|
||||||
public pickStatisticLabel?: string;
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
if (!this.hass) {
|
|
||||||
return html``;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
${this._currentStatistics.map(
|
|
||||||
(statisticId) => html`
|
|
||||||
<div>
|
|
||||||
<ha-statistic-picker
|
|
||||||
.curValue=${statisticId}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.value=${statisticId}
|
|
||||||
.statisticTypes=${this.statisticTypes}
|
|
||||||
.statisticIds=${this.statisticIds}
|
|
||||||
.label=${this.pickedStatisticLabel}
|
|
||||||
@value-changed=${this._statisticChanged}
|
|
||||||
></ha-statistic-picker>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<ha-statistic-picker
|
|
||||||
.hass=${this.hass}
|
|
||||||
.statisticTypes=${this.statisticTypes}
|
|
||||||
.statisticIds=${this.statisticIds}
|
|
||||||
.label=${this.pickStatisticLabel}
|
|
||||||
@value-changed=${this._addStatistic}
|
|
||||||
></ha-statistic-picker>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private get _currentStatistics() {
|
|
||||||
return this.value || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _updateStatistics(entities) {
|
|
||||||
this.value = entities;
|
|
||||||
|
|
||||||
fireEvent(this, "value-changed", {
|
|
||||||
value: entities,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _statisticChanged(event: PolymerChangedEvent<string>) {
|
|
||||||
event.stopPropagation();
|
|
||||||
const oldValue = (event.currentTarget as any).curValue;
|
|
||||||
const newValue = event.detail.value;
|
|
||||||
if (newValue === oldValue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const currentStatistics = this._currentStatistics;
|
|
||||||
if (!newValue || currentStatistics.includes(newValue)) {
|
|
||||||
this._updateStatistics(
|
|
||||||
currentStatistics.filter((ent) => ent !== oldValue)
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._updateStatistics(
|
|
||||||
currentStatistics.map((ent) => (ent === oldValue ? newValue : ent))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _addStatistic(event: PolymerChangedEvent<string>) {
|
|
||||||
event.stopPropagation();
|
|
||||||
const toAdd = event.detail.value;
|
|
||||||
if (!toAdd) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
(event.currentTarget as any).value = "";
|
|
||||||
if (!toAdd) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const currentEntities = this._currentStatistics;
|
|
||||||
if (currentEntities.includes(toAdd)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._updateStatistics([...currentEntities, toAdd]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-statistics-picker": HaStatisticsPicker;
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user