mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-04 08:29:52 +00:00
Compare commits
199 Commits
fix-more-i
...
20200306.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ad121f9e6 | ||
|
|
a0900afba3 | ||
|
|
1cb614c8a8 | ||
|
|
e63723f39e | ||
|
|
720bd03173 | ||
|
|
9e07cf67a5 | ||
|
|
503dec7345 | ||
|
|
5a2649a65b | ||
|
|
1599dc9e16 | ||
|
|
84dc8188c4 | ||
|
|
74657ae815 | ||
|
|
1a3b747d17 | ||
|
|
802db71400 | ||
|
|
4f98524258 | ||
|
|
b17ea09b8b | ||
|
|
8abbc71e91 | ||
|
|
1db31fb0f7 | ||
|
|
e9b5725d7b | ||
|
|
d3105b6846 | ||
|
|
196540afc7 | ||
|
|
2b8b9f8311 | ||
|
|
b4f0fce600 | ||
|
|
c6f101a487 | ||
|
|
54739c7ccd | ||
|
|
aa2e632df3 | ||
|
|
f3445d99cf | ||
|
|
7e48b21767 | ||
|
|
1d1688093a | ||
|
|
d392695ab7 | ||
|
|
5066560411 | ||
|
|
7fa6686e8c | ||
|
|
d74fe6ed52 | ||
|
|
319a3b4943 | ||
|
|
226e6e9f59 | ||
|
|
42f311a457 | ||
|
|
e50ec2e2e2 | ||
|
|
b72a3361c0 | ||
|
|
7b057eaa77 | ||
|
|
d7aaed05b7 | ||
|
|
c5fe5565bb | ||
|
|
a1a1763897 | ||
|
|
724357683c | ||
|
|
0d6de9fe73 | ||
|
|
5646045e9e | ||
|
|
17c7a3bbac | ||
|
|
8d65eb1fdf | ||
|
|
2298a55b16 | ||
|
|
33d65bcefc | ||
|
|
3cc7deda04 | ||
|
|
e2de660bec | ||
|
|
6b1e5a525f | ||
|
|
93565f0ed9 | ||
|
|
143d1162b6 | ||
|
|
788d616fa2 | ||
|
|
0de9471a5d | ||
|
|
b229071248 | ||
|
|
6d145730a5 | ||
|
|
f02bb67485 | ||
|
|
52ded635ff | ||
|
|
a6d73828b8 | ||
|
|
1d052fa5bb | ||
|
|
38d758b52f | ||
|
|
9162e9c318 | ||
|
|
189ea00768 | ||
|
|
25d6427aed | ||
|
|
8a61442cf2 | ||
|
|
106d405699 | ||
|
|
1f23e9062f | ||
|
|
231b498ea5 | ||
|
|
a256e5abfa | ||
|
|
028b370ead | ||
|
|
18abc6adf7 | ||
|
|
95aa29d6ca | ||
|
|
5d2242dd16 | ||
|
|
de8bca6967 | ||
|
|
12234de20e | ||
|
|
b41369a2ad | ||
|
|
6e35c79c14 | ||
|
|
22e4c0512e | ||
|
|
3606b8077f | ||
|
|
3a90a65ba8 | ||
|
|
e59987a8ed | ||
|
|
22d8ce0fd9 | ||
|
|
01eae3876b | ||
|
|
2e43f390a4 | ||
|
|
65421fa551 | ||
|
|
fc88922ce3 | ||
|
|
52609dded9 | ||
|
|
6d54496187 | ||
|
|
2a6c38066d | ||
|
|
924c7804c9 | ||
|
|
23f34fa7ae | ||
|
|
7046cba1f7 | ||
|
|
4be1040a14 | ||
|
|
68baeb83cb | ||
|
|
aa94e45582 | ||
|
|
2c58a9f802 | ||
|
|
0a41a4f066 | ||
|
|
e265d9581c | ||
|
|
4675579f79 | ||
|
|
52ae01ea74 | ||
|
|
099430238c | ||
|
|
af3626b215 | ||
|
|
52ea3a5ce8 | ||
|
|
2ab2ade642 | ||
|
|
1cc3936ec3 | ||
|
|
322eef1c0f | ||
|
|
0964130782 | ||
|
|
be9ec50e3a | ||
|
|
da1dd45169 | ||
|
|
9a7f7f119d | ||
|
|
b1a414c840 | ||
|
|
2718ada9f9 | ||
|
|
46a596ce34 | ||
|
|
7036cefa72 | ||
|
|
fb7fbf2dac | ||
|
|
49b0c8d549 | ||
|
|
8f9a6bd544 | ||
|
|
24e4b0b772 | ||
|
|
1c86bd2f8b | ||
|
|
363f548f13 | ||
|
|
51ce481e77 | ||
|
|
30e5611812 | ||
|
|
67706a312d | ||
|
|
f4eb3380b4 | ||
|
|
73934afc7d | ||
|
|
9d2a0c0502 | ||
|
|
9ec75531a8 | ||
|
|
91bdb8f742 | ||
|
|
d8ae3439de | ||
|
|
2d018fff6c | ||
|
|
7d37dc6cde | ||
|
|
c60033027d | ||
|
|
3f7c29a6f6 | ||
|
|
b2243f480c | ||
|
|
f5384e8bc8 | ||
|
|
ecc6fcf862 | ||
|
|
46cc2aec94 | ||
|
|
c62a5a6dcd | ||
|
|
f6b10232ec | ||
|
|
87559c0938 | ||
|
|
7903541689 | ||
|
|
c93e1b0123 | ||
|
|
e261fafdb3 | ||
|
|
485e2fde25 | ||
|
|
6feaf64c90 | ||
|
|
6b115bf06a | ||
|
|
f45785fafe | ||
|
|
ec046bc925 | ||
|
|
ab5733718b | ||
|
|
1077fb2945 | ||
|
|
b7a84cdd60 | ||
|
|
78102f5882 | ||
|
|
4ea11bd928 | ||
|
|
785aefa028 | ||
|
|
5c2004bcc1 | ||
|
|
156d944ca1 | ||
|
|
97a6354a72 | ||
|
|
49422c3f63 | ||
|
|
0b8700f725 | ||
|
|
c5aa000a97 | ||
|
|
4cdc4765f7 | ||
|
|
a95290235d | ||
|
|
fb9d7ac2d8 | ||
|
|
d48a4e0ac6 | ||
|
|
d33e035db7 | ||
|
|
1437b4c4b6 | ||
|
|
9fce60065b | ||
|
|
d052b9ede8 | ||
|
|
8cee5c729e | ||
|
|
88bdf7c7ec | ||
|
|
2c006e99f2 | ||
|
|
e7e8dff0ec | ||
|
|
981c798e22 | ||
|
|
4613d8b1f6 | ||
|
|
ba4e1949c4 | ||
|
|
cc6686a790 | ||
|
|
f791412f73 | ||
|
|
0c8ac17dcb | ||
|
|
9e11fe868e | ||
|
|
7d91515bf5 | ||
|
|
e0565c35ab | ||
|
|
e5387e5806 | ||
|
|
8a4c52aeb7 | ||
|
|
15e7b8117c | ||
|
|
d1703ba3e8 | ||
|
|
c977f22047 | ||
|
|
2e47aa1905 | ||
|
|
c72105dca3 | ||
|
|
e01f1cfcac | ||
|
|
50d0671abe | ||
|
|
e176357fbf | ||
|
|
de1b127ac2 | ||
|
|
1dad7c81da | ||
|
|
e980e93969 | ||
|
|
57fc56f836 | ||
|
|
05113e1809 | ||
|
|
1479ce9d56 | ||
|
|
ce8caa34f5 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -11,3 +11,4 @@
|
|||||||
*.mp3 binary
|
*.mp3 binary
|
||||||
|
|
||||||
demo/public/api/camera_proxy_stream/* binary
|
demo/public/api/camera_proxy_stream/* binary
|
||||||
|
demo/public/api/media_player_proxy/* binary
|
||||||
|
|||||||
23
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
23
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
@@ -3,6 +3,7 @@ name: Report a bug with the UI, Frontend or Lovelace
|
|||||||
about: Report an issue related to the Home Assistant frontend.
|
about: Report an issue related to the Home Assistant frontend.
|
||||||
labels: bug
|
labels: bug
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- READ THIS FIRST:
|
<!-- READ THIS FIRST:
|
||||||
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
|
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
|
||||||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
|
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
|
||||||
@@ -10,6 +11,7 @@ labels: bug
|
|||||||
- Provide as many details as possible. Paste logs, configuration samples and code into the backticks.
|
- Provide as many details as possible. Paste logs, configuration samples and code into the backticks.
|
||||||
DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed without comment.
|
DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed without comment.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
- [ ] I have updated to the latest available Home Assistant version.
|
- [ ] I have updated to the latest available Home Assistant version.
|
||||||
@@ -17,21 +19,22 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
|||||||
- [ ] I have tried a different browser to see if it is related to my browser.
|
- [ ] I have tried a different browser to see if it is related to my browser.
|
||||||
|
|
||||||
## The problem
|
## The problem
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Describe the issue you are experiencing here to communicate to the
|
Describe the issue you are experiencing here to communicate to the
|
||||||
maintainers. Tell us about the current behavior.
|
maintainers. Tell us about the current behavior.
|
||||||
If possible provide a screenshot with a description.
|
If possible provide a screenshot with a description.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
## Expected behavior
|
## Expected behavior
|
||||||
<!--
|
|
||||||
|
<!--
|
||||||
Describe what you expected to happen or it should look/behave.
|
Describe what you expected to happen or it should look/behave.
|
||||||
If possible provide a screenshot with a description.
|
If possible provide a screenshot with a description.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
## Steps to reproduce
|
## Steps to reproduce
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Provide steps for us, that helps reproducing your issue.
|
Provide steps for us, that helps reproducing your issue.
|
||||||
For example:
|
For example:
|
||||||
@@ -43,8 +46,8 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
|||||||
6. Set the HVAC action to cool
|
6. Set the HVAC action to cool
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
## Environment
|
## Environment
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Provide details about the versions you are using, which helps us reproducing
|
Provide details about the versions you are using, which helps us reproducing
|
||||||
and finding the issue quicker. Version information is found in the
|
and finding the issue quicker. Version information is found in the
|
||||||
@@ -54,13 +57,13 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
|||||||
your issue in a different browser and be sure to include your findings.
|
your issue in a different browser and be sure to include your findings.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
- Home Assistant release with the issue:
|
- Home Assistant release with the issue:
|
||||||
- Last working Home Assistant release (if known):
|
- Last working Home Assistant release (if known):
|
||||||
- UI Type (States or Lovelace):
|
- Browser and browser version:
|
||||||
- Browser and browser version:
|
- Operating system:
|
||||||
- Operating system:
|
|
||||||
|
|
||||||
## Problem-relevant configuration
|
## Problem-relevant configuration
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
An example configuration that caused the problem for you. Fill this out even
|
An example configuration that caused the problem for you. Fill this out even
|
||||||
if it seems unimportant to you. Please be sure to remove personal information
|
if it seems unimportant to you. Please be sure to remove personal information
|
||||||
@@ -72,6 +75,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Javascript errors shown in your browser console/inspector
|
## Javascript errors shown in your browser console/inspector
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
If you come across any javascript or other error logs, e.g., in your browser
|
If you come across any javascript or other error logs, e.g., in your browser
|
||||||
console/inspector please provide them.
|
console/inspector please provide them.
|
||||||
@@ -82,4 +86,3 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Additional information
|
## Additional information
|
||||||
|
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Report a bug that is NOT related to the UI, Frontend or Lovelace
|
- name: Report a bug that is NOT related to the UI, Frontend or Lovelace
|
||||||
url: https://github.com/home-assistant/home-assistant/issues
|
url: https://github.com/home-assistant/core/issues
|
||||||
about: This is the issue tracker for our frontend. Please report other issues with the backend repository.
|
about: This is the issue tracker for our frontend. Please report other issues with the backend repository.
|
||||||
- name: Report incorrect or missing information on our website
|
- name: Report incorrect or missing information on our website
|
||||||
url: https://github.com/home-assistant/home-assistant.io/issues
|
url: https://github.com/home-assistant/home-assistant.io/issues
|
||||||
|
|||||||
127
.github/workflows/ci.yaml
vendored
Normal file
127
.github/workflows/ci.yaml
vendored
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out files from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Setting up Node.js
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 12.x
|
||||||
|
- name: Get yarn cache path
|
||||||
|
id: yarn-cache-dir-path
|
||||||
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
|
- name: Fetching Yarn cache
|
||||||
|
uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-yarn-
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
- name: Build icons
|
||||||
|
run: ./node_modules/.bin/gulp gen-icons-hassio gen-icons-mdi gen-icons-app
|
||||||
|
- name: Build translations
|
||||||
|
run: ./node_modules/.bin/gulp build-translations
|
||||||
|
- name: Run eslint
|
||||||
|
run: ./node_modules/.bin/eslint src hassio/src gallery/src
|
||||||
|
- name: Run tslint
|
||||||
|
run: ./node_modules/.bin/tslint 'src/**/*.ts' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'cast/src/**/*.ts' 'test-mocha/**/*.ts'
|
||||||
|
- name: Run tsc
|
||||||
|
run: ./node_modules/.bin/tsc
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out files from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Setting up Node.js
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 12.x
|
||||||
|
- name: Get yarn cache path
|
||||||
|
id: yarn-cache-dir-path
|
||||||
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
|
- name: Fetching Yarn cache
|
||||||
|
uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-yarn-
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
- name: Run Mocha
|
||||||
|
run: npm run mocha
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [lint, test]
|
||||||
|
steps:
|
||||||
|
- name: Check out files from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Setting up Node.js
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 12.x
|
||||||
|
- name: Get yarn cache path
|
||||||
|
id: yarn-cache-dir-path
|
||||||
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
|
- name: Fetching Yarn cache
|
||||||
|
uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-yarn-
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
- name: Build Application
|
||||||
|
run: ./node_modules/.bin/gulp build-app
|
||||||
|
env:
|
||||||
|
TRAVIS: "true"
|
||||||
|
supervisor:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [lint, test]
|
||||||
|
steps:
|
||||||
|
- name: Check out files from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Setting up Node.js
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 12.x
|
||||||
|
- name: Get yarn cache path
|
||||||
|
id: yarn-cache-dir-path
|
||||||
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
|
- name: Fetching Yarn cache
|
||||||
|
uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-yarn-
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
- name: Build Application
|
||||||
|
run: ./node_modules/.bin/gulp build-hassio
|
||||||
|
env:
|
||||||
|
TRAVIS: "true"
|
||||||
39
.github/workflows/demo.yaml
vendored
Normal file
39
.github/workflows/demo.yaml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
name: Demo
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out files from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Setting up Node.js
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 12.x
|
||||||
|
- name: Get yarn cache path
|
||||||
|
id: yarn-cache-dir-path
|
||||||
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
|
- name: Fetching Yarn cache
|
||||||
|
uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-yarn-
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
- name: Build Demo
|
||||||
|
run: ./node_modules/.bin/gulp build-demo
|
||||||
|
- name: Deploy to Netlify
|
||||||
|
uses: netlify/actions/cli@master
|
||||||
|
env:
|
||||||
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
|
||||||
|
with:
|
||||||
|
args: deploy --dir=demo/dist --prod
|
||||||
18
.travis.yml
18
.travis.yml
@@ -1,18 +0,0 @@
|
|||||||
sudo: false
|
|
||||||
language: node_js
|
|
||||||
cache:
|
|
||||||
yarn: true
|
|
||||||
directories:
|
|
||||||
- bower_components
|
|
||||||
install: yarn install
|
|
||||||
script:
|
|
||||||
- npm run build
|
|
||||||
- hassio/script/build_hassio
|
|
||||||
# Because else eslint fails because hassio has cleaned that build
|
|
||||||
- ./node_modules/.bin/gulp gen-icons-app
|
|
||||||
- npm run test
|
|
||||||
# - xvfb-run wct --module-resolution=node --npm
|
|
||||||
# - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then wct --module-resolution=node --npm --plugin sauce; fi'
|
|
||||||
dist: trusty
|
|
||||||
addons:
|
|
||||||
sauce_connect: true
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Home Assistant Polymer [](https://travis-ci.org/home-assistant/home-assistant-polymer)
|
# Home Assistant Frontend
|
||||||
|
|
||||||
This is the repository for the official [Home Assistant](https://home-assistant.io) frontend.
|
This is the repository for the official [Home Assistant](https://home-assistant.io) frontend.
|
||||||
|
|
||||||
@@ -19,12 +19,15 @@ This is the repository for the official [Home Assistant](https://home-assistant.
|
|||||||
## Frontend development
|
## Frontend development
|
||||||
|
|
||||||
### Classic environment
|
### Classic environment
|
||||||
|
|
||||||
A complete guide can be found at the following [link](https://www.home-assistant.io/developers/frontend/). It describes a short guide for the build of project.
|
A complete guide can be found at the following [link](https://www.home-assistant.io/developers/frontend/). It describes a short guide for the build of project.
|
||||||
|
|
||||||
### Docker environment
|
### Docker environment
|
||||||
|
|
||||||
It is possible to compile the project and/or run commands in the development environment having only the [Docker](https://www.docker.com) pre-installed in the system. On the root of project you can do:
|
It is possible to compile the project and/or run commands in the development environment having only the [Docker](https://www.docker.com) pre-installed in the system. On the root of project you can do:
|
||||||
* `sh ./script/docker_run.sh build` Build all the project with one command
|
|
||||||
* `sh ./script/docker_run.sh bash` Open an interactive shell (the same environment generated by the *classic environment*) where you can run commands. This bash work on your project directory and any change on your file is automatically present within your build bash.
|
- `sh ./script/docker_run.sh build` Build all the project with one command
|
||||||
|
- `sh ./script/docker_run.sh bash` Open an interactive shell (the same environment generated by the _classic environment_) where you can run commands. This bash work on your project directory and any change on your file is automatically present within your build bash.
|
||||||
|
|
||||||
**Note**: if you have installed `npm` in addition to the `docker`, you can use the commands `npm run docker_build` and `npm run bash` to get a full build or bash as explained above
|
**Note**: if you have installed `npm` in addition to the `docker`, you can use the commands `npm run docker_build` and `npm run bash` to get a full build or bash as explained above
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"@babel/plugin-proposal-optional-chaining",
|
"@babel/plugin-proposal-optional-chaining",
|
||||||
|
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||||
[
|
[
|
||||||
require("@babel/plugin-proposal-decorators").default,
|
require("@babel/plugin-proposal-decorators").default,
|
||||||
{ decoratorsBeforeExport: true },
|
{ decoratorsBeforeExport: true },
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ gulp.task(
|
|||||||
gulp.parallel("gen-icons-app", "gen-icons-mdi"),
|
gulp.parallel("gen-icons-app", "gen-icons-mdi"),
|
||||||
"gen-pages-dev",
|
"gen-pages-dev",
|
||||||
"gen-index-app-dev",
|
"gen-index-app-dev",
|
||||||
gulp.series("create-test-translation", "build-translations")
|
"build-translations"
|
||||||
),
|
),
|
||||||
"copy-static",
|
"copy-static",
|
||||||
"webpack-watch-app"
|
"webpack-watch-app"
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ function copyMapPanel(staticDir) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gulp.task("copy-translations", (done) => {
|
||||||
|
const staticDir = paths.static;
|
||||||
|
copyTranslations(staticDir);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
gulp.task("copy-static", (done) => {
|
gulp.task("copy-static", (done) => {
|
||||||
const staticDir = paths.static;
|
const staticDir = paths.static;
|
||||||
const staticPath = genStaticPath(paths.static);
|
const staticPath = genStaticPath(paths.static);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const gulp = require("gulp");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const paths = require("../paths");
|
const paths = require("../paths");
|
||||||
|
const { mapFiles } = require("../util");
|
||||||
|
|
||||||
const ICON_PACKAGE_PATH = path.resolve(
|
const ICON_PACKAGE_PATH = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
@@ -57,20 +58,6 @@ function generateIconset(iconsetName, iconNames) {
|
|||||||
return `<ha-iconset-svg name="${iconsetName}" size="24"><svg><defs>${iconDefs}</defs></svg></ha-iconset-svg>`;
|
return `<ha-iconset-svg name="${iconsetName}" size="24"><svg><defs>${iconDefs}</defs></svg></ha-iconset-svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to map recursively over files in a folder and it's subfolders
|
|
||||||
function mapFiles(startPath, filter, mapFunc) {
|
|
||||||
const files = fs.readdirSync(startPath);
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
const filename = path.join(startPath, files[i]);
|
|
||||||
const stat = fs.lstatSync(filename);
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
mapFiles(filename, filter, mapFunc);
|
|
||||||
} else if (filename.indexOf(filter) >= 0) {
|
|
||||||
mapFunc(filename);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find all icons used by the project.
|
// Find all icons used by the project.
|
||||||
function findIcons(searchPath, iconsetName) {
|
function findIcons(searchPath, iconsetName) {
|
||||||
const iconRegex = new RegExp(`${iconsetName}:[\\w-]+`, "g");
|
const iconRegex = new RegExp(`${iconsetName}:[\\w-]+`, "g");
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
|
const crypto = require("crypto");
|
||||||
const del = require("del");
|
const del = require("del");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const source = require("vinyl-source-stream");
|
||||||
|
const vinylBuffer = require("vinyl-buffer");
|
||||||
const gulp = require("gulp");
|
const gulp = require("gulp");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const foreach = require("gulp-foreach");
|
const foreach = require("gulp-foreach");
|
||||||
const hash = require("gulp-hash");
|
|
||||||
const hashFilename = require("gulp-hash-filename");
|
|
||||||
const merge = require("gulp-merge-json");
|
const merge = require("gulp-merge-json");
|
||||||
const minify = require("gulp-jsonminify");
|
const minify = require("gulp-jsonminify");
|
||||||
const rename = require("gulp-rename");
|
const rename = require("gulp-rename");
|
||||||
const transform = require("gulp-json-transform");
|
const transform = require("gulp-json-transform");
|
||||||
|
const { mapFiles } = require("../util");
|
||||||
|
const env = require("../env");
|
||||||
|
const paths = require("../paths");
|
||||||
|
|
||||||
const inDir = "translations";
|
const inDir = "translations";
|
||||||
const workDir = "build-translations";
|
const workDir = "build-translations";
|
||||||
@@ -39,8 +43,6 @@ const TRANSLATION_FRAGMENTS = [
|
|||||||
"developer-tools",
|
"developer-tools",
|
||||||
];
|
];
|
||||||
|
|
||||||
const tasks = [];
|
|
||||||
|
|
||||||
function recursiveFlatten(prefix, data) {
|
function recursiveFlatten(prefix, data) {
|
||||||
let output = {};
|
let output = {};
|
||||||
Object.keys(data).forEach(function(key) {
|
Object.keys(data).forEach(function(key) {
|
||||||
@@ -116,11 +118,9 @@ function lokaliseTransform(data, original, file) {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
let taskName = "clean-translations";
|
gulp.task("clean-translations", function() {
|
||||||
gulp.task(taskName, function() {
|
return del([workDir]);
|
||||||
return del([`${outDir}/**/*.json`]);
|
|
||||||
});
|
});
|
||||||
tasks.push(taskName);
|
|
||||||
|
|
||||||
gulp.task("ensure-translations-build-dir", (done) => {
|
gulp.task("ensure-translations-build-dir", (done) => {
|
||||||
if (!fs.existsSync(workDir)) {
|
if (!fs.existsSync(workDir)) {
|
||||||
@@ -129,29 +129,23 @@ gulp.task("ensure-translations-build-dir", (done) => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
taskName = "create-test-metadata";
|
gulp.task("create-test-metadata", function(cb) {
|
||||||
gulp.task(
|
fs.writeFile(
|
||||||
taskName,
|
workDir + "/testMetadata.json",
|
||||||
gulp.series("ensure-translations-build-dir", function writeTestMetaData(cb) {
|
JSON.stringify({
|
||||||
fs.writeFile(
|
test: {
|
||||||
workDir + "/testMetadata.json",
|
nativeName: "Test",
|
||||||
JSON.stringify({
|
},
|
||||||
test: {
|
}),
|
||||||
nativeName: "Test",
|
cb
|
||||||
},
|
);
|
||||||
}),
|
});
|
||||||
cb
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
tasks.push(taskName);
|
|
||||||
|
|
||||||
taskName = "create-test-translation";
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
taskName,
|
"create-test-translation",
|
||||||
gulp.series("create-test-metadata", function() {
|
gulp.series("create-test-metadata", function createTestTranslation() {
|
||||||
return gulp
|
return gulp
|
||||||
.src("src/translations/en.json")
|
.src(path.join(paths.translations_src, "en.json"))
|
||||||
.pipe(
|
.pipe(
|
||||||
transform(function(data, file) {
|
transform(function(data, file) {
|
||||||
return recursiveEmpty(data);
|
return recursiveEmpty(data);
|
||||||
@@ -161,7 +155,6 @@ gulp.task(
|
|||||||
.pipe(gulp.dest(workDir));
|
.pipe(gulp.dest(workDir));
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
tasks.push(taskName);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This task will build a master translation file, to be used as the base for
|
* This task will build a master translation file, to be used as the base for
|
||||||
@@ -172,235 +165,215 @@ tasks.push(taskName);
|
|||||||
* project is buildable immediately after merging new translation keys, since
|
* project is buildable immediately after merging new translation keys, since
|
||||||
* the Lokalise update to translations/en.json will not happen immediately.
|
* the Lokalise update to translations/en.json will not happen immediately.
|
||||||
*/
|
*/
|
||||||
taskName = "build-master-translation";
|
gulp.task("build-master-translation", function() {
|
||||||
gulp.task(
|
return gulp
|
||||||
taskName,
|
.src(path.join(paths.translations_src, "en.json"))
|
||||||
gulp.series("clean-translations", function() {
|
.pipe(
|
||||||
return gulp
|
transform(function(data, file) {
|
||||||
.src("src/translations/en.json")
|
return lokaliseTransform(data, data, file);
|
||||||
.pipe(
|
})
|
||||||
transform(function(data, file) {
|
)
|
||||||
return lokaliseTransform(data, data, file);
|
.pipe(rename("translationMaster.json"))
|
||||||
})
|
.pipe(gulp.dest(workDir));
|
||||||
)
|
});
|
||||||
.pipe(rename("translationMaster.json"))
|
|
||||||
.pipe(gulp.dest(workDir));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
tasks.push(taskName);
|
|
||||||
|
|
||||||
taskName = "build-merged-translations";
|
gulp.task("build-merged-translations", function() {
|
||||||
gulp.task(
|
return gulp
|
||||||
taskName,
|
.src([inDir + "/*.json", workDir + "/test.json"], { allowEmpty: true })
|
||||||
gulp.series("build-master-translation", function() {
|
.pipe(
|
||||||
return gulp
|
transform(function(data, file) {
|
||||||
.src([inDir + "/*.json", workDir + "/test.json"], { allowEmpty: true })
|
return lokaliseTransform(data, data, file);
|
||||||
.pipe(
|
})
|
||||||
transform(function(data, file) {
|
)
|
||||||
return lokaliseTransform(data, data, file);
|
.pipe(
|
||||||
})
|
foreach(function(stream, file) {
|
||||||
)
|
// For each language generate a merged json file. It begins with the master
|
||||||
.pipe(
|
// translation as a failsafe for untranslated strings, and merges all parent
|
||||||
foreach(function(stream, file) {
|
// tags into one file for each specific subtag
|
||||||
// For each language generate a merged json file. It begins with the master
|
//
|
||||||
// translation as a failsafe for untranslated strings, and merges all parent
|
// TODO: This is a naive interpretation of BCP47 that should be improved.
|
||||||
// tags into one file for each specific subtag
|
// Will be OK for now as long as we don't have anything more complicated
|
||||||
//
|
// than a base translation + region.
|
||||||
// TODO: This is a naive interpretation of BCP47 that should be improved.
|
const tr = path.basename(file.history[0], ".json");
|
||||||
// Will be OK for now as long as we don't have anything more complicated
|
const subtags = tr.split("-");
|
||||||
// than a base translation + region.
|
const src = [workDir + "/translationMaster.json"];
|
||||||
const tr = path.basename(file.history[0], ".json");
|
for (let i = 1; i <= subtags.length; i++) {
|
||||||
const subtags = tr.split("-");
|
const lang = subtags.slice(0, i).join("-");
|
||||||
const src = [workDir + "/translationMaster.json"];
|
if (lang === "test") {
|
||||||
for (let i = 1; i <= subtags.length; i++) {
|
src.push(workDir + "/test.json");
|
||||||
const lang = subtags.slice(0, i).join("-");
|
} else if (lang !== "en") {
|
||||||
if (lang === "test") {
|
src.push(inDir + "/" + lang + ".json");
|
||||||
src.push(workDir + "/test.json");
|
|
||||||
} else if (lang !== "en") {
|
|
||||||
src.push(inDir + "/" + lang + ".json");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return gulp
|
}
|
||||||
.src(src, { allowEmpty: true })
|
return gulp
|
||||||
.pipe(transform((data) => emptyFilter(data)))
|
.src(src, { allowEmpty: true })
|
||||||
.pipe(
|
.pipe(transform((data) => emptyFilter(data)))
|
||||||
merge({
|
.pipe(
|
||||||
fileName: tr + ".json",
|
merge({
|
||||||
})
|
fileName: tr + ".json",
|
||||||
)
|
})
|
||||||
.pipe(gulp.dest(fullDir));
|
)
|
||||||
})
|
.pipe(gulp.dest(fullDir));
|
||||||
);
|
})
|
||||||
})
|
);
|
||||||
);
|
});
|
||||||
tasks.push(taskName);
|
|
||||||
|
var taskName;
|
||||||
|
|
||||||
const splitTasks = [];
|
const splitTasks = [];
|
||||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||||
taskName = "build-translation-fragment-" + fragment;
|
taskName = "build-translation-fragment-" + fragment;
|
||||||
gulp.task(
|
gulp.task(taskName, function() {
|
||||||
taskName,
|
// Return only the translations for this fragment.
|
||||||
gulp.series("build-merged-translations", function() {
|
return gulp
|
||||||
// Return only the translations for this fragment.
|
.src(fullDir + "/*.json")
|
||||||
return gulp
|
.pipe(
|
||||||
.src(fullDir + "/*.json")
|
transform((data) => ({
|
||||||
.pipe(
|
ui: {
|
||||||
transform((data) => ({
|
panel: {
|
||||||
ui: {
|
[fragment]: data.ui.panel[fragment],
|
||||||
panel: {
|
|
||||||
[fragment]: data.ui.panel[fragment],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}))
|
},
|
||||||
)
|
}))
|
||||||
.pipe(gulp.dest(workDir + "/" + fragment));
|
)
|
||||||
})
|
.pipe(gulp.dest(workDir + "/" + fragment));
|
||||||
);
|
});
|
||||||
tasks.push(taskName);
|
|
||||||
splitTasks.push(taskName);
|
splitTasks.push(taskName);
|
||||||
});
|
});
|
||||||
|
|
||||||
taskName = "build-translation-core";
|
taskName = "build-translation-core";
|
||||||
gulp.task(
|
gulp.task(taskName, function() {
|
||||||
taskName,
|
// Remove the fragment translations from the core translation.
|
||||||
gulp.series("build-merged-translations", function() {
|
return gulp
|
||||||
// Remove the fragment translations from the core translation.
|
.src(fullDir + "/*.json")
|
||||||
return gulp
|
.pipe(
|
||||||
.src(fullDir + "/*.json")
|
transform((data) => {
|
||||||
.pipe(
|
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||||
transform((data) => {
|
delete data.ui.panel[fragment];
|
||||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
});
|
||||||
delete data.ui.panel[fragment];
|
return data;
|
||||||
});
|
})
|
||||||
return data;
|
)
|
||||||
})
|
.pipe(gulp.dest(coreDir));
|
||||||
)
|
});
|
||||||
.pipe(gulp.dest(coreDir));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
tasks.push(taskName);
|
|
||||||
splitTasks.push(taskName);
|
splitTasks.push(taskName);
|
||||||
|
|
||||||
taskName = "build-flattened-translations";
|
gulp.task("build-flattened-translations", function() {
|
||||||
gulp.task(
|
// Flatten the split versions of our translations, and move them into outDir
|
||||||
taskName,
|
return gulp
|
||||||
gulp.series(...splitTasks, function() {
|
.src(
|
||||||
// Flatten the split versions of our translations, and move them into outDir
|
TRANSLATION_FRAGMENTS.map(
|
||||||
return gulp
|
(fragment) => workDir + "/" + fragment + "/*.json"
|
||||||
.src(
|
).concat(coreDir + "/*.json"),
|
||||||
TRANSLATION_FRAGMENTS.map(
|
{ base: workDir }
|
||||||
(fragment) => workDir + "/" + fragment + "/*.json"
|
)
|
||||||
).concat(coreDir + "/*.json"),
|
.pipe(
|
||||||
{ base: workDir }
|
transform(function(data) {
|
||||||
)
|
// Polymer.AppLocalizeBehavior requires flattened json
|
||||||
.pipe(
|
return flatten(data);
|
||||||
transform(function(data) {
|
})
|
||||||
// Polymer.AppLocalizeBehavior requires flattened json
|
)
|
||||||
return flatten(data);
|
.pipe(minify())
|
||||||
})
|
.pipe(
|
||||||
)
|
rename((filePath) => {
|
||||||
.pipe(minify())
|
if (filePath.dirname === "core") {
|
||||||
.pipe(hashFilename())
|
filePath.dirname = "";
|
||||||
.pipe(
|
}
|
||||||
rename((filePath) => {
|
})
|
||||||
if (filePath.dirname === "core") {
|
)
|
||||||
filePath.dirname = "";
|
.pipe(gulp.dest(outDir));
|
||||||
}
|
});
|
||||||
})
|
|
||||||
)
|
|
||||||
.pipe(gulp.dest(outDir));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
tasks.push(taskName);
|
|
||||||
|
|
||||||
taskName = "build-translation-fingerprints";
|
const fingerprints = {};
|
||||||
gulp.task(
|
|
||||||
taskName,
|
|
||||||
gulp.series("build-flattened-translations", function() {
|
|
||||||
return gulp
|
|
||||||
.src(outDir + "/**/*.json")
|
|
||||||
.pipe(
|
|
||||||
rename({
|
|
||||||
extname: "",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.pipe(
|
|
||||||
hash({
|
|
||||||
algorithm: "md5",
|
|
||||||
hashLength: 32,
|
|
||||||
template: "<%= name %>.json",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.pipe(hash.manifest("translationFingerprints.json"))
|
|
||||||
.pipe(
|
|
||||||
transform(function(data) {
|
|
||||||
// After generating fingerprints of our translation files, consolidate
|
|
||||||
// all translation fragment fingerprints under the translation name key
|
|
||||||
const newData = {};
|
|
||||||
Object.entries(data).forEach(([key, value]) => {
|
|
||||||
const [path, _md5] = key.rsplit("-", 1);
|
|
||||||
// let translation = key;
|
|
||||||
let translation = path;
|
|
||||||
const parts = translation.split("/");
|
|
||||||
if (parts.length === 2) {
|
|
||||||
translation = parts[1];
|
|
||||||
}
|
|
||||||
if (!(translation in newData)) {
|
|
||||||
newData[translation] = {
|
|
||||||
fingerprints: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
newData[translation].fingerprints[path] = value;
|
|
||||||
});
|
|
||||||
return newData;
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.pipe(gulp.dest(workDir));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
tasks.push(taskName);
|
|
||||||
|
|
||||||
taskName = "build-translations";
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
taskName,
|
"build-translation-fingerprints",
|
||||||
gulp.series("build-translation-fingerprints", function() {
|
function fingerprintTranslationFiles() {
|
||||||
return gulp
|
// Fingerprint full file of each language
|
||||||
.src(
|
const files = fs.readdirSync(fullDir);
|
||||||
[
|
for (let i = 0; i < files.length; i++) {
|
||||||
"src/translations/translationMetadata.json",
|
fingerprints[files[i].split(".")[0]] = {
|
||||||
workDir + "/testMetadata.json",
|
// In dev we create fake hashes
|
||||||
workDir + "/translationFingerprints.json",
|
hash: env.isProdBuild
|
||||||
],
|
? crypto
|
||||||
{ allowEmpty: true }
|
.createHash("md5")
|
||||||
)
|
.update(fs.readFileSync(path.join(fullDir, files[i]), "utf-8"))
|
||||||
.pipe(merge({}))
|
.digest("hex")
|
||||||
.pipe(
|
: "dev",
|
||||||
transform(function(data) {
|
};
|
||||||
const newData = {};
|
}
|
||||||
Object.entries(data).forEach(([key, value]) => {
|
|
||||||
// Filter out translations without native name.
|
|
||||||
if (data[key].nativeName) {
|
|
||||||
newData[key] = data[key];
|
|
||||||
} else {
|
|
||||||
console.warn(
|
|
||||||
`Skipping language ${key}. Native name was not translated.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (data[key]) newData[key] = value;
|
|
||||||
});
|
|
||||||
return newData;
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.pipe(
|
|
||||||
transform((data) => ({
|
|
||||||
fragments: TRANSLATION_FRAGMENTS,
|
|
||||||
translations: data,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
.pipe(rename("translationMetadata.json"))
|
|
||||||
.pipe(gulp.dest(workDir));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
tasks.push(taskName);
|
|
||||||
|
|
||||||
module.exports = tasks;
|
mapFiles(outDir, ".json", (filename) => {
|
||||||
|
const parsed = path.parse(filename);
|
||||||
|
|
||||||
|
// nl.json -> nl-<hash>.json
|
||||||
|
if (!(parsed.name in fingerprints)) {
|
||||||
|
throw new Error(`Unable to find hash for ${filename}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.renameSync(
|
||||||
|
filename,
|
||||||
|
`${parsed.dir}/${parsed.name}-${fingerprints[parsed.name].hash}${
|
||||||
|
parsed.ext
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const stream = source("translationFingerprints.json");
|
||||||
|
stream.write(JSON.stringify(fingerprints));
|
||||||
|
process.nextTick(() => stream.end());
|
||||||
|
return stream.pipe(vinylBuffer()).pipe(gulp.dest(workDir));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"build-translations",
|
||||||
|
gulp.series(
|
||||||
|
"clean-translations",
|
||||||
|
"ensure-translations-build-dir",
|
||||||
|
env.isProdBuild ? (done) => done() : "create-test-translation",
|
||||||
|
"build-master-translation",
|
||||||
|
"build-merged-translations",
|
||||||
|
gulp.parallel(...splitTasks),
|
||||||
|
"build-flattened-translations",
|
||||||
|
"build-translation-fingerprints",
|
||||||
|
function writeMetadata() {
|
||||||
|
return gulp
|
||||||
|
.src(
|
||||||
|
[
|
||||||
|
path.join(paths.translations_src, "translationMetadata.json"),
|
||||||
|
workDir + "/testMetadata.json",
|
||||||
|
workDir + "/translationFingerprints.json",
|
||||||
|
],
|
||||||
|
{ allowEmpty: true }
|
||||||
|
)
|
||||||
|
.pipe(merge({}))
|
||||||
|
.pipe(
|
||||||
|
transform(function(data) {
|
||||||
|
const newData = {};
|
||||||
|
Object.entries(data).forEach(([key, value]) => {
|
||||||
|
// Filter out translations without native name.
|
||||||
|
if (data[key].nativeName) {
|
||||||
|
newData[key] = data[key];
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
`Skipping language ${key}. Native name was not translated.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data[key]) newData[key] = value;
|
||||||
|
});
|
||||||
|
return newData;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.pipe(
|
||||||
|
transform((data) => ({
|
||||||
|
fragments: TRANSLATION_FRAGMENTS,
|
||||||
|
translations: data,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
.pipe(rename("translationMetadata.json"))
|
||||||
|
.pipe(gulp.dest(workDir));
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const gulp = require("gulp");
|
|||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
const WebpackDevServer = require("webpack-dev-server");
|
const WebpackDevServer = require("webpack-dev-server");
|
||||||
const log = require("fancy-log");
|
const log = require("fancy-log");
|
||||||
|
const path = require("path");
|
||||||
const paths = require("../paths");
|
const paths = require("../paths");
|
||||||
const {
|
const {
|
||||||
createAppConfig,
|
createAppConfig,
|
||||||
@@ -57,10 +58,14 @@ const handler = (done) => (err, stats) => {
|
|||||||
|
|
||||||
gulp.task("webpack-watch-app", () => {
|
gulp.task("webpack-watch-app", () => {
|
||||||
// we are not calling done, so this command will run forever
|
// we are not calling done, so this command will run forever
|
||||||
webpack(bothBuilds(createAppConfig, { isProdBuild: false })).watch(
|
webpack(createAppConfig({ isProdBuild: false, latestBuild: true })).watch(
|
||||||
{},
|
{ ignored: /build-translations/ },
|
||||||
handler()
|
handler()
|
||||||
);
|
);
|
||||||
|
gulp.watch(
|
||||||
|
path.join(paths.translations_src, "en.json"),
|
||||||
|
gulp.series("build-translations", "copy-translations")
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
|
|||||||
@@ -29,4 +29,6 @@ module.exports = {
|
|||||||
hassio_dir: path.resolve(__dirname, "../hassio"),
|
hassio_dir: path.resolve(__dirname, "../hassio"),
|
||||||
hassio_root: path.resolve(__dirname, "../hassio/build"),
|
hassio_root: path.resolve(__dirname, "../hassio/build"),
|
||||||
hassio_publicPath: "/api/hassio/app/",
|
hassio_publicPath: "/api/hassio/app/",
|
||||||
|
|
||||||
|
translations_src: path.resolve(__dirname, "../src/translations"),
|
||||||
};
|
};
|
||||||
|
|||||||
16
build-scripts/util.js
Normal file
16
build-scripts/util.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
// Helper function to map recursively over files in a folder and it's subfolders
|
||||||
|
module.exports.mapFiles = function mapFiles(startPath, filter, mapFunc) {
|
||||||
|
const files = fs.readdirSync(startPath);
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const filename = path.join(startPath, files[i]);
|
||||||
|
const stat = fs.lstatSync(filename);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
mapFiles(filename, filter, mapFunc);
|
||||||
|
} else if (filename.indexOf(filter) >= 0) {
|
||||||
|
mapFunc(filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -148,11 +148,17 @@ const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
|
|||||||
// Create an object mapping browser urls to their paths during build
|
// Create an object mapping browser urls to their paths during build
|
||||||
const translationMetadata = require("../build-translations/translationMetadata.json");
|
const translationMetadata = require("../build-translations/translationMetadata.json");
|
||||||
const workBoxTranslationsTemplatedURLs = {};
|
const workBoxTranslationsTemplatedURLs = {};
|
||||||
const englishFP = translationMetadata.translations.en.fingerprints;
|
const englishFilename = `en-${translationMetadata.translations.en.hash}.json`;
|
||||||
Object.keys(englishFP).forEach((key) => {
|
|
||||||
|
// core
|
||||||
|
workBoxTranslationsTemplatedURLs[
|
||||||
|
`/static/translations/${englishFilename}`
|
||||||
|
] = `build-translations/output/${englishFilename}`;
|
||||||
|
|
||||||
|
Object.keys(translationMetadata.fragments).forEach((fragment) => {
|
||||||
workBoxTranslationsTemplatedURLs[
|
workBoxTranslationsTemplatedURLs[
|
||||||
`/static/translations/${englishFP[key]}`
|
`/static/translations/${fragment}/${englishFilename}`
|
||||||
] = `build-translations/output/${key}.json`;
|
] = `build-translations/output/${fragment}/${englishFilename}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
config.plugins.push(
|
config.plugins.push(
|
||||||
|
|||||||
@@ -26,10 +26,12 @@ import { CastManager } from "../../../../src/cast/cast_manager";
|
|||||||
import {
|
import {
|
||||||
LovelaceConfig,
|
LovelaceConfig,
|
||||||
getLovelaceCollection,
|
getLovelaceCollection,
|
||||||
|
getLegacyLovelaceCollection,
|
||||||
} from "../../../../src/data/lovelace";
|
} from "../../../../src/data/lovelace";
|
||||||
import "./hc-layout";
|
import "./hc-layout";
|
||||||
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
|
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
|
||||||
import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute";
|
import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute";
|
||||||
|
import { atLeastVersion } from "../../../../src/common/config/version";
|
||||||
|
|
||||||
@customElement("hc-cast")
|
@customElement("hc-cast")
|
||||||
class HcCast extends LitElement {
|
class HcCast extends LitElement {
|
||||||
@@ -133,7 +135,9 @@ class HcCast extends LitElement {
|
|||||||
protected firstUpdated(changedProps) {
|
protected firstUpdated(changedProps) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
|
|
||||||
const llColl = getLovelaceCollection(this.connection);
|
const llColl = atLeastVersion(this.connection.haVersion, 0, 107)
|
||||||
|
? getLovelaceCollection(this.connection)
|
||||||
|
: getLegacyLovelaceCollection(this.connection);
|
||||||
// We first do a single refresh because we need to check if there is LL
|
// We first do a single refresh because we need to check if there is LL
|
||||||
// configuration.
|
// configuration.
|
||||||
llColl.refresh().then(
|
llColl.refresh().then(
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import {
|
|||||||
import {
|
import {
|
||||||
LovelaceConfig,
|
LovelaceConfig,
|
||||||
getLovelaceCollection,
|
getLovelaceCollection,
|
||||||
|
fetchResources,
|
||||||
|
LegacyLovelaceConfig,
|
||||||
|
getLegacyLovelaceCollection,
|
||||||
} from "../../../../src/data/lovelace";
|
} from "../../../../src/data/lovelace";
|
||||||
import "./hc-launch-screen";
|
import "./hc-launch-screen";
|
||||||
import { castContext } from "../cast_context";
|
import { castContext } from "../cast_context";
|
||||||
@@ -22,6 +25,9 @@ import { CAST_NS } from "../../../../src/cast/const";
|
|||||||
import { ReceiverStatusMessage } from "../../../../src/cast/sender_messages";
|
import { ReceiverStatusMessage } from "../../../../src/cast/sender_messages";
|
||||||
import { loadLovelaceResources } from "../../../../src/panels/lovelace/common/load-resources";
|
import { loadLovelaceResources } from "../../../../src/panels/lovelace/common/load-resources";
|
||||||
import { isNavigationClick } from "../../../../src/common/dom/is-navigation-click";
|
import { isNavigationClick } from "../../../../src/common/dom/is-navigation-click";
|
||||||
|
import { atLeastVersion } from "../../../../src/common/config/version";
|
||||||
|
|
||||||
|
let resourcesLoaded = false;
|
||||||
|
|
||||||
@customElement("hc-main")
|
@customElement("hc-main")
|
||||||
export class HcMain extends HassElement {
|
export class HcMain extends HassElement {
|
||||||
@@ -34,6 +40,7 @@ export class HcMain extends HassElement {
|
|||||||
@property() private _error?: string;
|
@property() private _error?: string;
|
||||||
|
|
||||||
private _unsubLovelace?: UnsubscribeFunc;
|
private _unsubLovelace?: UnsubscribeFunc;
|
||||||
|
private _urlPath?: string | null;
|
||||||
|
|
||||||
public processIncomingMessage(msg: HassMessage) {
|
public processIncomingMessage(msg: HassMessage) {
|
||||||
if (msg.type === "connect") {
|
if (msg.type === "connect") {
|
||||||
@@ -108,6 +115,7 @@ export class HcMain extends HassElement {
|
|||||||
if (this.hass) {
|
if (this.hass) {
|
||||||
status.hassUrl = this.hass.auth.data.hassUrl;
|
status.hassUrl = this.hass.auth.data.hassUrl;
|
||||||
status.lovelacePath = this._lovelacePath!;
|
status.lovelacePath = this._lovelacePath!;
|
||||||
|
status.urlPath = this._urlPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (senderId) {
|
if (senderId) {
|
||||||
@@ -163,8 +171,14 @@ export class HcMain extends HassElement {
|
|||||||
this._error = "Cannot show Lovelace because we're not connected.";
|
this._error = "Cannot show Lovelace because we're not connected.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this._unsubLovelace) {
|
if (!this._unsubLovelace || this._urlPath !== msg.urlPath) {
|
||||||
const llColl = getLovelaceCollection(this.hass!.connection);
|
this._urlPath = msg.urlPath;
|
||||||
|
if (this._unsubLovelace) {
|
||||||
|
this._unsubLovelace();
|
||||||
|
}
|
||||||
|
const llColl = atLeastVersion(this.hass.connection.haVersion, 0, 107)
|
||||||
|
? getLovelaceCollection(this.hass!.connection, msg.urlPath)
|
||||||
|
: getLegacyLovelaceCollection(this.hass!.connection);
|
||||||
// We first do a single refresh because we need to check if there is LL
|
// We first do a single refresh because we need to check if there is LL
|
||||||
// configuration.
|
// configuration.
|
||||||
try {
|
try {
|
||||||
@@ -183,6 +197,15 @@ export class HcMain extends HassElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!resourcesLoaded) {
|
||||||
|
resourcesLoaded = true;
|
||||||
|
const resources = atLeastVersion(this.hass.connection.haVersion, 0, 107)
|
||||||
|
? await fetchResources(this.hass!.connection)
|
||||||
|
: (this._lovelaceConfig as LegacyLovelaceConfig).resources;
|
||||||
|
if (resources) {
|
||||||
|
loadLovelaceResources(resources, this.hass!.auth.data.hassUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
this._showDemo = false;
|
this._showDemo = false;
|
||||||
this._lovelacePath = msg.viewPath;
|
this._lovelacePath = msg.viewPath;
|
||||||
if (castContext.getDeviceCapabilities().touch_input_supported) {
|
if (castContext.getDeviceCapabilities().touch_input_supported) {
|
||||||
@@ -194,12 +217,6 @@ export class HcMain extends HassElement {
|
|||||||
private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) {
|
private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) {
|
||||||
castContext.setApplicationState(lovelaceConfig.title!);
|
castContext.setApplicationState(lovelaceConfig.title!);
|
||||||
this._lovelaceConfig = lovelaceConfig;
|
this._lovelaceConfig = lovelaceConfig;
|
||||||
if (lovelaceConfig.resources) {
|
|
||||||
loadLovelaceResources(
|
|
||||||
lovelaceConfig.resources,
|
|
||||||
this.hass!.auth.data.hassUrl
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleShowDemo(_msg: ShowDemoMessage) {
|
private _handleShowDemo(_msg: ShowDemoMessage) {
|
||||||
|
|||||||
BIN
demo/public/api/media_player_proxy/media_player.family_room_2
Normal file
BIN
demo/public/api/media_player_proxy/media_player.family_room_2
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 20 KiB |
@@ -395,7 +395,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
|||||||
cards: [
|
cards: [
|
||||||
{
|
{
|
||||||
entity: "script.air_cleaner_quiet",
|
entity: "script.air_cleaner_quiet",
|
||||||
type: "entity-button",
|
type: "button",
|
||||||
name: "AC bed",
|
name: "AC bed",
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "call-service",
|
action: "call-service",
|
||||||
@@ -408,7 +408,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
entity: "script.air_cleaner_auto",
|
entity: "script.air_cleaner_auto",
|
||||||
type: "entity-button",
|
type: "button",
|
||||||
name: "AC bed",
|
name: "AC bed",
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "call-service",
|
action: "call-service",
|
||||||
@@ -421,7 +421,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
entity: "script.air_cleaner_turbo",
|
entity: "script.air_cleaner_turbo",
|
||||||
type: "entity-button",
|
type: "button",
|
||||||
name: "AC bed",
|
name: "AC bed",
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "call-service",
|
action: "call-service",
|
||||||
@@ -434,7 +434,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
entity: "script.ac_off",
|
entity: "script.ac_off",
|
||||||
type: "entity-button",
|
type: "button",
|
||||||
name: "AC",
|
name: "AC",
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "call-service",
|
action: "call-service",
|
||||||
@@ -447,7 +447,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
entity: "script.ac_on",
|
entity: "script.ac_on",
|
||||||
type: "entity-button",
|
type: "button",
|
||||||
name: "AC",
|
name: "AC",
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "call-service",
|
action: "call-service",
|
||||||
@@ -658,7 +658,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
|||||||
action: "call-service",
|
action: "call-service",
|
||||||
service: "script.goodnight",
|
service: "script.goodnight",
|
||||||
},
|
},
|
||||||
type: "entity-button",
|
type: "button",
|
||||||
icon: "mdi:weather-night",
|
icon: "mdi:weather-night",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -670,7 +670,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
|||||||
},
|
},
|
||||||
service: "scene.turn_on",
|
service: "scene.turn_on",
|
||||||
},
|
},
|
||||||
type: "entity-button",
|
type: "button",
|
||||||
icon: "mdi:coffee-outline",
|
icon: "mdi:coffee-outline",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -682,7 +682,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
|||||||
},
|
},
|
||||||
service: "scene.turn_on",
|
service: "scene.turn_on",
|
||||||
},
|
},
|
||||||
type: "entity-button",
|
type: "button",
|
||||||
icon: "mdi:television-classic",
|
icon: "mdi:television-classic",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -743,7 +743,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
|||||||
},
|
},
|
||||||
service: "light.toggle",
|
service: "light.toggle",
|
||||||
},
|
},
|
||||||
type: "entity-button",
|
type: "button",
|
||||||
icon: "mdi:page-layout-footer",
|
icon: "mdi:page-layout-footer",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -755,7 +755,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
|||||||
},
|
},
|
||||||
service: "light.toggle",
|
service: "light.toggle",
|
||||||
},
|
},
|
||||||
type: "entity-button",
|
type: "button",
|
||||||
icon: "mdi:page-layout-header",
|
icon: "mdi:page-layout-header",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ const CONFIGS = [
|
|||||||
{
|
{
|
||||||
heading: "Basic example",
|
heading: "Basic example",
|
||||||
config: `
|
config: `
|
||||||
- type: entity-button
|
- type: button
|
||||||
entity: light.bed_light
|
entity: light.bed_light
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "With Name",
|
heading: "With Name",
|
||||||
config: `
|
config: `
|
||||||
- type: entity-button
|
- type: button
|
||||||
name: Bedroom
|
name: Bedroom
|
||||||
entity: light.bed_light
|
entity: light.bed_light
|
||||||
`,
|
`,
|
||||||
@@ -30,7 +30,7 @@ const CONFIGS = [
|
|||||||
{
|
{
|
||||||
heading: "With Icon",
|
heading: "With Icon",
|
||||||
config: `
|
config: `
|
||||||
- type: entity-button
|
- type: button
|
||||||
entity: light.bed_light
|
entity: light.bed_light
|
||||||
icon: mdi:hotel
|
icon: mdi:hotel
|
||||||
`,
|
`,
|
||||||
@@ -38,7 +38,7 @@ const CONFIGS = [
|
|||||||
{
|
{
|
||||||
heading: "Without State",
|
heading: "Without State",
|
||||||
config: `
|
config: `
|
||||||
- type: entity-button
|
- type: button
|
||||||
entity: light.bed_light
|
entity: light.bed_light
|
||||||
show_state: false
|
show_state: false
|
||||||
`,
|
`,
|
||||||
@@ -46,7 +46,7 @@ const CONFIGS = [
|
|||||||
{
|
{
|
||||||
heading: "Custom Tap Action (toggle)",
|
heading: "Custom Tap Action (toggle)",
|
||||||
config: `
|
config: `
|
||||||
- type: entity-button
|
- type: button
|
||||||
entity: light.bed_light
|
entity: light.bed_light
|
||||||
tap_action:
|
tap_action:
|
||||||
action: toggle
|
action: toggle
|
||||||
@@ -55,7 +55,7 @@ const CONFIGS = [
|
|||||||
{
|
{
|
||||||
heading: "Running Service",
|
heading: "Running Service",
|
||||||
config: `
|
config: `
|
||||||
- type: entity-button
|
- type: button
|
||||||
entity: light.bed_light
|
entity: light.bed_light
|
||||||
service: light.toggle
|
service: light.toggle
|
||||||
`,
|
`,
|
||||||
@@ -63,13 +63,13 @@ const CONFIGS = [
|
|||||||
{
|
{
|
||||||
heading: "Invalid Entity",
|
heading: "Invalid Entity",
|
||||||
config: `
|
config: `
|
||||||
- type: entity-button
|
- type: button
|
||||||
entity: sensor.invalid_entity
|
entity: sensor.invalid_entity
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
class DemoEntityButtonEntity extends PolymerElement {
|
class DemoButtonEntity extends PolymerElement {
|
||||||
static get template() {
|
static get template() {
|
||||||
return html`
|
return html`
|
||||||
<demo-cards
|
<demo-cards
|
||||||
@@ -97,4 +97,4 @@ class DemoEntityButtonEntity extends PolymerElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("demo-hui-entity-button-card", DemoEntityButtonEntity);
|
customElements.define("demo-hui-button-card", DemoButtonEntity);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from "../../../src/data/hassio/addon";
|
} from "../../../src/data/hassio/addon";
|
||||||
import { navigate } from "../../../src/common/navigate";
|
import { navigate } from "../../../src/common/navigate";
|
||||||
import { filterAndSort } from "../components/hassio-filter-addons";
|
import { filterAndSort } from "../components/hassio-filter-addons";
|
||||||
|
import { atLeastVersion } from "../../../src/common/config/version";
|
||||||
|
|
||||||
class HassioAddonRepositoryEl extends LitElement {
|
class HassioAddonRepositoryEl extends LitElement {
|
||||||
@property() public hass!: HomeAssistant;
|
@property() public hass!: HomeAssistant;
|
||||||
@@ -39,7 +40,6 @@ class HassioAddonRepositoryEl extends LitElement {
|
|||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const repo = this.repo;
|
const repo = this.repo;
|
||||||
const addons = this._getAddons(this.addons, this.filter);
|
const addons = this._getAddons(this.addons, this.filter);
|
||||||
const ha105pluss = this._computeHA105plus;
|
|
||||||
|
|
||||||
if (this.filter && addons.length < 1) {
|
if (this.filter && addons.length < 1) {
|
||||||
return html`
|
return html`
|
||||||
@@ -57,7 +57,9 @@ class HassioAddonRepositoryEl extends LitElement {
|
|||||||
</h1>
|
</h1>
|
||||||
<p class="description">
|
<p class="description">
|
||||||
Maintained by ${repo.maintainer}<br />
|
Maintained by ${repo.maintainer}<br />
|
||||||
<a class="repo" href=${repo.url} target="_blank">${repo.url}</a>
|
<a class="repo" href=${repo.url} target="_blank" rel="noreferrer">
|
||||||
|
${repo.url}
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<div class="card-group">
|
<div class="card-group">
|
||||||
${addons.map(
|
${addons.map(
|
||||||
@@ -90,7 +92,11 @@ class HassioAddonRepositoryEl extends LitElement {
|
|||||||
: !addon.available
|
: !addon.available
|
||||||
? "not_available"
|
? "not_available"
|
||||||
: ""}
|
: ""}
|
||||||
.iconImage=${ha105pluss && addon.icon
|
.iconImage=${atLeastVersion(
|
||||||
|
this.hass.connection.haVersion,
|
||||||
|
0,
|
||||||
|
105
|
||||||
|
) && addon.icon
|
||||||
? `/api/hassio/addons/${addon.slug}/icon`
|
? `/api/hassio/addons/${addon.slug}/icon`
|
||||||
: undefined}
|
: undefined}
|
||||||
.showTopbar=${addon.installed || !addon.available}
|
.showTopbar=${addon.installed || !addon.available}
|
||||||
@@ -115,11 +121,6 @@ class HassioAddonRepositoryEl extends LitElement {
|
|||||||
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`);
|
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _computeHA105plus(): boolean {
|
|
||||||
const [major, minor] = this.hass.config.version.split(".", 2);
|
|
||||||
return Number(major) > 0 || (major === "0" && Number(minor) >= 105);
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultArray {
|
static get styles(): CSSResultArray {
|
||||||
return [
|
return [
|
||||||
hassioStyle,
|
hassioStyle,
|
||||||
|
|||||||
@@ -128,22 +128,27 @@ class HassioAddonAudio extends LitElement {
|
|||||||
|
|
||||||
private _setInputDevice(ev): void {
|
private _setInputDevice(ev): void {
|
||||||
const device = ev.detail.item.getAttribute("device");
|
const device = ev.detail.item.getAttribute("device");
|
||||||
this._selectedInput = device || null;
|
this._selectedInput = device;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setOutputDevice(ev): void {
|
private _setOutputDevice(ev): void {
|
||||||
const device = ev.detail.item.getAttribute("device");
|
const device = ev.detail.item.getAttribute("device");
|
||||||
this._selectedOutput = device || null;
|
this._selectedOutput = device;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _addonChanged(): Promise<void> {
|
private async _addonChanged(): Promise<void> {
|
||||||
this._selectedInput = this.addon.audio_input;
|
this._selectedInput =
|
||||||
this._selectedOutput = this.addon.audio_output;
|
this.addon.audio_input === null ? "default" : this.addon.audio_input;
|
||||||
|
this._selectedOutput =
|
||||||
|
this.addon.audio_output === null ? "default" : this.addon.audio_output;
|
||||||
if (this._outputDevices) {
|
if (this._outputDevices) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const noDevice: HassioHardwareAudioDevice = { device: null, name: "-" };
|
const noDevice: HassioHardwareAudioDevice = {
|
||||||
|
device: "default",
|
||||||
|
name: "Default",
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { audio } = await fetchHassioHardwareAudio(this.hass);
|
const { audio } = await fetchHassioHardwareAudio(this.hass);
|
||||||
@@ -168,8 +173,10 @@ class HassioAddonAudio extends LitElement {
|
|||||||
private async _saveSettings(): Promise<void> {
|
private async _saveSettings(): Promise<void> {
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
const data: HassioAddonSetOptionParams = {
|
const data: HassioAddonSetOptionParams = {
|
||||||
audio_input: this._selectedInput || null,
|
audio_input:
|
||||||
audio_output: this._selectedOutput || null,
|
this._selectedInput === "default" ? null : this._selectedInput,
|
||||||
|
audio_output:
|
||||||
|
this._selectedOutput === "default" ? null : this._selectedOutput,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { haStyle } from "../../../src/resources/styles";
|
|||||||
import { HomeAssistant } from "../../../src/types";
|
import { HomeAssistant } from "../../../src/types";
|
||||||
import { navigate } from "../../../src/common/navigate";
|
import { navigate } from "../../../src/common/navigate";
|
||||||
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
|
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
|
||||||
|
import { atLeastVersion } from "../../../src/common/config/version";
|
||||||
|
|
||||||
const PERMIS_DESC = {
|
const PERMIS_DESC = {
|
||||||
rating: {
|
rating: {
|
||||||
@@ -185,14 +186,19 @@ class HassioAddonInfo extends LitElement {
|
|||||||
<div class="description light-color">
|
<div class="description light-color">
|
||||||
${this.addon.description}.<br />
|
${this.addon.description}.<br />
|
||||||
Visit
|
Visit
|
||||||
<a href="${this.addon.url}" target="_blank">
|
<a href="${this.addon.url}" target="_blank" rel="noreferrer">
|
||||||
${this.addon.name} page</a
|
${this.addon.name} page</a
|
||||||
>
|
>
|
||||||
for details.
|
for details.
|
||||||
</div>
|
</div>
|
||||||
${this.addon.logo
|
${this.addon.logo
|
||||||
? html`
|
? html`
|
||||||
<a href="${this.addon.url}" target="_blank" class="logo">
|
<a
|
||||||
|
href="${this.addon.url}"
|
||||||
|
target="_blank"
|
||||||
|
class="logo"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
<img src="/api/hassio/addons/${this.addon.slug}/logo" />
|
<img src="/api/hassio/addons/${this.addon.slug}/logo" />
|
||||||
</a>
|
</a>
|
||||||
`
|
`
|
||||||
@@ -428,6 +434,7 @@ class HassioAddonInfo extends LitElement {
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="right"
|
class="right"
|
||||||
|
rel="noopener"
|
||||||
>
|
>
|
||||||
<mwc-button>
|
<mwc-button>
|
||||||
Open web UI
|
Open web UI
|
||||||
@@ -452,7 +459,7 @@ class HassioAddonInfo extends LitElement {
|
|||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
<ha-progress-button
|
<ha-progress-button
|
||||||
.disabled=${!this.addon.available}
|
.disabled=${!this.addon.available || this._installing}
|
||||||
.progress=${this._installing}
|
.progress=${this._installing}
|
||||||
@click=${this._installClicked}
|
@click=${this._installClicked}
|
||||||
>
|
>
|
||||||
@@ -653,7 +660,10 @@ class HassioAddonInfo extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private get _computeCannotIngressSidebar(): boolean {
|
private get _computeCannotIngressSidebar(): boolean {
|
||||||
return !this.addon.ingress || !this._computeHA92plus;
|
return (
|
||||||
|
!this.addon.ingress ||
|
||||||
|
!atLeastVersion(this.hass.connection.haVersion, 0, 92)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _computeUsesProtectedOptions(): boolean {
|
private get _computeUsesProtectedOptions(): boolean {
|
||||||
@@ -662,11 +672,6 @@ class HassioAddonInfo extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _computeHA92plus(): boolean {
|
|
||||||
const [major, minor] = this.hass.config.version.split(".", 2);
|
|
||||||
return Number(major) > 0 || (major === "0" && Number(minor) >= 92);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _startOnBootToggled(): Promise<void> {
|
private async _startOnBootToggled(): Promise<void> {
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
const data: HassioAddonSetOptionParams = {
|
const data: HassioAddonSetOptionParams = {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { navigate } from "../../../src/common/navigate";
|
|||||||
import { hassioStyle } from "../resources/hassio-style";
|
import { hassioStyle } from "../resources/hassio-style";
|
||||||
import { haStyle } from "../../../src/resources/styles";
|
import { haStyle } from "../../../src/resources/styles";
|
||||||
import "../components/hassio-card-content";
|
import "../components/hassio-card-content";
|
||||||
|
import { atLeastVersion } from "../../../src/common/config/version";
|
||||||
|
|
||||||
@customElement("hassio-addons")
|
@customElement("hassio-addons")
|
||||||
class HassioAddons extends LitElement {
|
class HassioAddons extends LitElement {
|
||||||
@@ -22,9 +23,6 @@ class HassioAddons extends LitElement {
|
|||||||
@property() public addons?: HassioAddonInfo[];
|
@property() public addons?: HassioAddonInfo[];
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const [major, minor] = this.hass.config.version.split(".", 2);
|
|
||||||
const ha105pluss =
|
|
||||||
Number(major) > 0 || (major === "0" && Number(minor) >= 105);
|
|
||||||
return html`
|
return html`
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h1>Add-ons</h1>
|
<h1>Add-ons</h1>
|
||||||
@@ -68,7 +66,11 @@ class HassioAddons extends LitElement {
|
|||||||
: addon.installed && addon.state === "started"
|
: addon.installed && addon.state === "started"
|
||||||
? "running"
|
? "running"
|
||||||
: "stopped"}
|
: "stopped"}
|
||||||
.iconImage=${ha105pluss && addon.icon
|
.iconImage=${atLeastVersion(
|
||||||
|
this.hass.connection.haVersion,
|
||||||
|
0,
|
||||||
|
105
|
||||||
|
) && addon.icon
|
||||||
? `/api/hassio/addons/${addon.slug}/icon`
|
? `/api/hassio/addons/${addon.slug}/icon`
|
||||||
: undefined}
|
: undefined}
|
||||||
></hassio-card-content>
|
></hassio-card-content>
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ export class HassioUpdate extends LitElement {
|
|||||||
!!value &&
|
!!value &&
|
||||||
(value.last_version
|
(value.last_version
|
||||||
? value.version !== value.last_version
|
? value.version !== value.last_version
|
||||||
: value.version !== value.version_latest)
|
: value.version_latest
|
||||||
|
? value.version !== value.version_latest
|
||||||
|
: false)
|
||||||
);
|
);
|
||||||
}).length;
|
}).length;
|
||||||
|
|
||||||
@@ -102,7 +104,7 @@ export class HassioUpdate extends LitElement {
|
|||||||
releaseNotesUrl: string,
|
releaseNotesUrl: string,
|
||||||
icon?: string
|
icon?: string
|
||||||
): TemplateResult {
|
): TemplateResult {
|
||||||
if (lastVersion === curVersion) {
|
if (!lastVersion || lastVersion === curVersion) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
@@ -121,7 +123,7 @@ export class HassioUpdate extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="${releaseNotesUrl}" target="_blank">
|
<a href="${releaseNotesUrl}" target="_blank" rel="noreferrer">
|
||||||
<mwc-button>Release notes</mwc-button>
|
<mwc-button>Release notes</mwc-button>
|
||||||
</a>
|
</a>
|
||||||
<ha-call-api-button
|
<ha-call-api-button
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ class HassioSupervisorInfo extends LitElement {
|
|||||||
!confirm(`WARNING:
|
!confirm(`WARNING:
|
||||||
Beta releases are for testers and early adopters and can contain unstable code changes. Make sure you have backups of your data before you activate this feature.
|
Beta releases are for testers and early adopters and can contain unstable code changes. Make sure you have backups of your data before you activate this feature.
|
||||||
|
|
||||||
This inludes beta releases for:
|
This includes beta releases for:
|
||||||
- Home Assistant (Release Candidates)
|
- Home Assistant (Release Candidates)
|
||||||
- Hass.io supervisor
|
- Hass.io supervisor
|
||||||
- Host system`)
|
- Host system`)
|
||||||
|
|||||||
63
package.json
63
package.json
@@ -18,15 +18,13 @@
|
|||||||
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material/chips": "^3.2.0",
|
"@material/chips": "^5.0.0",
|
||||||
"@material/data-table": "^3.2.0",
|
"@material/mwc-button": "^0.13.0",
|
||||||
"@material/mwc-base": "^0.10.0",
|
"@material/mwc-checkbox": "^0.13.0",
|
||||||
"@material/mwc-button": "^0.10.0",
|
"@material/mwc-dialog": "^0.13.0",
|
||||||
"@material/mwc-checkbox": "^0.10.0",
|
"@material/mwc-fab": "^0.13.0",
|
||||||
"@material/mwc-dialog": "^0.10.0",
|
"@material/mwc-ripple": "^0.13.0",
|
||||||
"@material/mwc-fab": "^0.10.0",
|
"@material/mwc-switch": "^0.13.0",
|
||||||
"@material/mwc-ripple": "^0.10.0",
|
|
||||||
"@material/mwc-switch": "^0.10.0",
|
|
||||||
"@mdi/svg": "4.9.95",
|
"@mdi/svg": "4.9.95",
|
||||||
"@polymer/app-layout": "^3.0.2",
|
"@polymer/app-layout": "^3.0.2",
|
||||||
"@polymer/app-localize-behavior": "^3.0.1",
|
"@polymer/app-localize-behavior": "^3.0.1",
|
||||||
@@ -70,8 +68,9 @@
|
|||||||
"@polymer/paper-tooltip": "^3.0.1",
|
"@polymer/paper-tooltip": "^3.0.1",
|
||||||
"@polymer/polymer": "3.1.0",
|
"@polymer/polymer": "3.1.0",
|
||||||
"@thomasloven/round-slider": "0.3.7",
|
"@thomasloven/round-slider": "0.3.7",
|
||||||
"@vaadin/vaadin-combo-box": "^5.0.6",
|
"@types/resize-observer-browser": "^0.1.3",
|
||||||
"@vaadin/vaadin-date-picker": "^4.0.3",
|
"@vaadin/vaadin-combo-box": "^5.0.10",
|
||||||
|
"@vaadin/vaadin-date-picker": "^4.0.7",
|
||||||
"@webcomponents/shadycss": "^1.9.0",
|
"@webcomponents/shadycss": "^1.9.0",
|
||||||
"@webcomponents/webcomponentsjs": "^2.2.7",
|
"@webcomponents/webcomponentsjs": "^2.2.7",
|
||||||
"chart.js": "~2.8.0",
|
"chart.js": "~2.8.0",
|
||||||
@@ -79,12 +78,13 @@
|
|||||||
"codemirror": "^5.49.0",
|
"codemirror": "^5.49.0",
|
||||||
"cpx": "^1.5.0",
|
"cpx": "^1.5.0",
|
||||||
"deep-clone-simple": "^1.1.1",
|
"deep-clone-simple": "^1.1.1",
|
||||||
|
"deep-freeze": "^0.0.1",
|
||||||
"es6-object-assign": "^1.1.0",
|
"es6-object-assign": "^1.1.0",
|
||||||
"fecha": "^3.0.2",
|
"fecha": "^3.0.2",
|
||||||
"fuse.js": "^3.4.4",
|
"fuse.js": "^3.4.4",
|
||||||
"google-timezones-json": "^1.0.2",
|
"google-timezones-json": "^1.0.2",
|
||||||
"hls.js": "^0.12.4",
|
"hls.js": "^0.12.4",
|
||||||
"home-assistant-js-websocket": "^4.4.0",
|
"home-assistant-js-websocket": "4.5.0",
|
||||||
"intl-messageformat": "^2.2.0",
|
"intl-messageformat": "^2.2.0",
|
||||||
"js-yaml": "^3.13.1",
|
"js-yaml": "^3.13.1",
|
||||||
"leaflet": "^1.4.0",
|
"leaflet": "^1.4.0",
|
||||||
@@ -96,10 +96,12 @@
|
|||||||
"mdn-polyfills": "^5.16.0",
|
"mdn-polyfills": "^5.16.0",
|
||||||
"memoize-one": "^5.0.2",
|
"memoize-one": "^5.0.2",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
|
"node-vibrant": "^3.1.5",
|
||||||
"preact": "^8.4.2",
|
"preact": "^8.4.2",
|
||||||
"preact-compat": "^3.18.4",
|
"preact-compat": "^3.18.4",
|
||||||
"react-big-calendar": "^0.20.4",
|
"react-big-calendar": "^0.20.4",
|
||||||
"regenerator-runtime": "^0.13.2",
|
"regenerator-runtime": "^0.13.2",
|
||||||
|
"resize-observer": "^1.0.0",
|
||||||
"roboto-fontface": "^0.10.0",
|
"roboto-fontface": "^0.10.0",
|
||||||
"superstruct": "^0.6.1",
|
"superstruct": "^0.6.1",
|
||||||
"tslib": "^1.10.0",
|
"tslib": "^1.10.0",
|
||||||
@@ -108,16 +110,17 @@
|
|||||||
"xss": "^1.0.6"
|
"xss": "^1.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.7.4",
|
"@babel/core": "^7.8.4",
|
||||||
"@babel/plugin-external-helpers": "^7.7.4",
|
"@babel/plugin-external-helpers": "^7.8.3",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||||
"@babel/plugin-proposal-decorators": "^7.7.4",
|
"@babel/plugin-proposal-decorators": "^7.8.3",
|
||||||
"@babel/plugin-proposal-object-rest-spread": "^7.7.4",
|
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.7.4",
|
"@babel/plugin-proposal-object-rest-spread": "^7.8.3",
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
|
"@babel/plugin-proposal-optional-chaining": "^7.8.3",
|
||||||
"@babel/plugin-transform-react-jsx": "^7.7.4",
|
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||||
"@babel/preset-env": "^7.7.4",
|
"@babel/plugin-transform-react-jsx": "^7.8.3",
|
||||||
"@babel/preset-typescript": "^7.7.4",
|
"@babel/preset-env": "^7.8.4",
|
||||||
|
"@babel/preset-typescript": "^7.8.3",
|
||||||
"@types/chai": "^4.1.7",
|
"@types/chai": "^4.1.7",
|
||||||
"@types/chromecast-caf-receiver": "^3.0.12",
|
"@types/chromecast-caf-receiver": "^3.0.12",
|
||||||
"@types/chromecast-caf-sender": "^1.0.1",
|
"@types/chromecast-caf-sender": "^1.0.1",
|
||||||
@@ -143,13 +146,11 @@
|
|||||||
"fs-extra": "^7.0.1",
|
"fs-extra": "^7.0.1",
|
||||||
"gulp": "^4.0.0",
|
"gulp": "^4.0.0",
|
||||||
"gulp-foreach": "^0.1.0",
|
"gulp-foreach": "^0.1.0",
|
||||||
"gulp-hash": "^4.2.2",
|
|
||||||
"gulp-hash-filename": "^2.0.1",
|
|
||||||
"gulp-insert": "^0.5.0",
|
"gulp-insert": "^0.5.0",
|
||||||
"gulp-json-transform": "^0.4.6",
|
"gulp-json-transform": "^0.4.6",
|
||||||
"gulp-jsonminify": "^1.1.0",
|
"gulp-jsonminify": "^1.1.0",
|
||||||
"gulp-merge-json": "^1.3.1",
|
"gulp-merge-json": "^1.3.1",
|
||||||
"gulp-rename": "^1.4.0",
|
"gulp-rename": "^2.0.0",
|
||||||
"gulp-zopfli-green": "^3.0.1",
|
"gulp-zopfli-green": "^3.0.1",
|
||||||
"html-loader": "^0.5.5",
|
"html-loader": "^0.5.5",
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
@@ -172,6 +173,8 @@
|
|||||||
"tslint-eslint-rules": "^5.4.0",
|
"tslint-eslint-rules": "^5.4.0",
|
||||||
"tslint-plugin-prettier": "^2.0.1",
|
"tslint-plugin-prettier": "^2.0.1",
|
||||||
"typescript": "^3.7.2",
|
"typescript": "^3.7.2",
|
||||||
|
"vinyl-buffer": "^1.0.1",
|
||||||
|
"vinyl-source-stream": "^2.0.0",
|
||||||
"web-component-tester": "^6.9.2",
|
"web-component-tester": "^6.9.2",
|
||||||
"webpack": "^4.40.2",
|
"webpack": "^4.40.2",
|
||||||
"webpack-cli": "^3.3.9",
|
"webpack-cli": "^3.3.9",
|
||||||
@@ -185,7 +188,15 @@
|
|||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@webcomponents/webcomponentsjs": "^2.2.10",
|
"@webcomponents/webcomponentsjs": "^2.2.10",
|
||||||
"@polymer/polymer": "3.1.0",
|
"@polymer/polymer": "3.1.0",
|
||||||
"lit-html": "^1.1.2"
|
"lit-html": "^1.1.2",
|
||||||
|
"@material/button": "^5.0.0",
|
||||||
|
"@material/checkbox": "^5.0.0",
|
||||||
|
"@material/dialog": "^5.0.0",
|
||||||
|
"@material/fab": "^5.0.0",
|
||||||
|
"@material/switch": "^5.0.0",
|
||||||
|
"@material/ripple": "^5.0.0",
|
||||||
|
"@material/dom": "^5.0.0",
|
||||||
|
"@material/touch-target": "^5.0.0"
|
||||||
},
|
},
|
||||||
"main": "src/home-assistant.js",
|
"main": "src/home-assistant.js",
|
||||||
"husky": {
|
"husky": {
|
||||||
|
|||||||
@@ -11,17 +11,13 @@
|
|||||||
"src/panels/dev-template/ha-panel-dev-template.js",
|
"src/panels/dev-template/ha-panel-dev-template.js",
|
||||||
"src/panels/history/ha-panel-history.js",
|
"src/panels/history/ha-panel-history.js",
|
||||||
"src/panels/iframe/ha-panel-iframe.js",
|
"src/panels/iframe/ha-panel-iframe.js",
|
||||||
"src/panels/kiosk/ha-panel-kiosk.js",
|
|
||||||
"src/panels/logbook/ha-panel-logbook.js",
|
"src/panels/logbook/ha-panel-logbook.js",
|
||||||
"src/panels/map/ha-panel-map.js",
|
"src/panels/map/ha-panel-map.js",
|
||||||
"src/panels/shopping-list/ha-panel-shopping-list.js",
|
"src/panels/shopping-list/ha-panel-shopping-list.js",
|
||||||
"src/panels/mailbox/ha-panel-mailbox.js",
|
"src/panels/mailbox/ha-panel-mailbox.js",
|
||||||
"hassio/src/entrypoint.js"
|
"hassio/src/entrypoint.js"
|
||||||
],
|
],
|
||||||
"sources": [
|
"sources": ["src/**/*", "!src/translations/*"],
|
||||||
"src/**/*",
|
|
||||||
"!src/translations/*"
|
|
||||||
],
|
|
||||||
"lint": {
|
"lint": {
|
||||||
"rules": ["polymer-3"],
|
"rules": ["polymer-3"],
|
||||||
"ignoreWarnings": ["could-not-resolve-reference", "could-not-load"],
|
"ignoreWarnings": ["could-not-resolve-reference", "could-not-load"],
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="home-assistant-frontend",
|
name="home-assistant-frontend",
|
||||||
version="20200130.0",
|
version="20200306.0",
|
||||||
description="The Home Assistant frontend",
|
description="The Home Assistant frontend",
|
||||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||||
author="The Home Assistant Authors",
|
author="The Home Assistant Authors",
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import { TemplateResult, html } from "lit-html";
|
|
||||||
import { customElement, LitElement, property } from "lit-element";
|
|
||||||
import { HassEntity } from "home-assistant-js-websocket";
|
|
||||||
|
|
||||||
import "../components/entity/ha-state-label-badge";
|
|
||||||
|
|
||||||
import { HomeAssistant } from "../types";
|
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
|
||||||
|
|
||||||
@customElement("ha-badges-card")
|
|
||||||
class HaBadgesCard extends LitElement {
|
|
||||||
@property() public hass?: HomeAssistant;
|
|
||||||
@property() public states?: HassEntity[];
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
if (!this.hass || !this.states) {
|
|
||||||
return html``;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
${this.states.map(
|
|
||||||
(state) => html`
|
|
||||||
<ha-state-label-badge
|
|
||||||
.hass=${this.hass}
|
|
||||||
.state=${state}
|
|
||||||
@click=${this._handleClick}
|
|
||||||
></ha-state-label-badge>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleClick(ev: Event) {
|
|
||||||
const entityId = ((ev.target as any).state as HassEntity).entity_id;
|
|
||||||
fireEvent(this, "hass-more-info", {
|
|
||||||
entityId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-badges-card": HaBadgesCard;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import "@polymer/paper-styles/element-styles/paper-material-styles";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
|
|
||||||
import { computeStateName } from "../common/entity/compute_state_name";
|
|
||||||
import { EventsMixin } from "../mixins/events-mixin";
|
|
||||||
import LocalizeMixin from "../mixins/localize-mixin";
|
|
||||||
import { fetchThumbnailUrlWithCache } from "../data/camera";
|
|
||||||
|
|
||||||
const UPDATE_INTERVAL = 10000; // ms
|
|
||||||
/*
|
|
||||||
* @appliesMixin LocalizeMixin
|
|
||||||
* @appliesMixin EventsMixin
|
|
||||||
*/
|
|
||||||
class HaCameraCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style include="paper-material-styles">
|
|
||||||
:host {
|
|
||||||
@apply --paper-material-elevation-1;
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
font-size: 0px;
|
|
||||||
border-radius: 2px;
|
|
||||||
cursor: pointer;
|
|
||||||
min-height: 48px;
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
.camera-feed {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
.caption {
|
|
||||||
@apply --paper-font-common-nowrap;
|
|
||||||
position: absolute;
|
|
||||||
left: 0px;
|
|
||||||
right: 0px;
|
|
||||||
bottom: 0px;
|
|
||||||
border-bottom-left-radius: 2px;
|
|
||||||
border-bottom-right-radius: 2px;
|
|
||||||
|
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
|
||||||
padding: 16px;
|
|
||||||
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 16px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<template is="dom-if" if="[[cameraFeedSrc]]">
|
|
||||||
<img
|
|
||||||
src="[[cameraFeedSrc]]"
|
|
||||||
class="camera-feed"
|
|
||||||
alt="[[_computeStateName(stateObj)]]"
|
|
||||||
on-load="_imageLoaded"
|
|
||||||
on-error="_imageError"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<div class="caption">
|
|
||||||
[[_computeStateName(stateObj)]]
|
|
||||||
<template is="dom-if" if="[[!imageLoaded]]">
|
|
||||||
([[localize('ui.card.camera.not_available')]])
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: Object,
|
|
||||||
stateObj: {
|
|
||||||
type: Object,
|
|
||||||
observer: "updateCameraFeedSrc",
|
|
||||||
},
|
|
||||||
cameraFeedSrc: {
|
|
||||||
type: String,
|
|
||||||
value: "",
|
|
||||||
},
|
|
||||||
imageLoaded: {
|
|
||||||
type: Boolean,
|
|
||||||
value: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ready() {
|
|
||||||
super.ready();
|
|
||||||
this.addEventListener("click", () => this.cardTapped());
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this.timer = setInterval(() => this.updateCameraFeedSrc(), UPDATE_INTERVAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
clearInterval(this.timer);
|
|
||||||
}
|
|
||||||
|
|
||||||
_imageLoaded() {
|
|
||||||
this.imageLoaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_imageError() {
|
|
||||||
this.imageLoaded = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
cardTapped() {
|
|
||||||
this.fire("hass-more-info", { entityId: this.stateObj.entity_id });
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateCameraFeedSrc() {
|
|
||||||
this.cameraFeedSrc = await fetchThumbnailUrlWithCache(
|
|
||||||
this.hass,
|
|
||||||
this.stateObj.entity_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeStateName(stateObj) {
|
|
||||||
return computeStateName(stateObj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define("ha-camera-card", HaCameraCard);
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
|
|
||||||
import "./ha-camera-card";
|
|
||||||
import "./ha-entities-card";
|
|
||||||
import "./ha-history_graph-card";
|
|
||||||
import "./ha-media_player-card";
|
|
||||||
import "./ha-persistent_notification-card";
|
|
||||||
import "./ha-plant-card";
|
|
||||||
import "./ha-weather-card";
|
|
||||||
|
|
||||||
import dynamicContentUpdater from "../common/dom/dynamic_content_updater";
|
|
||||||
|
|
||||||
class HaCardChooser extends PolymerElement {
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
cardData: {
|
|
||||||
type: Object,
|
|
||||||
observer: "cardDataChanged",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateCard(newData) {
|
|
||||||
dynamicContentUpdater(
|
|
||||||
this,
|
|
||||||
"HA-" + newData.cardType.toUpperCase() + "-CARD",
|
|
||||||
newData
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
createObserver() {
|
|
||||||
this._updatesAllowed = false;
|
|
||||||
this.observer = new IntersectionObserver((entries) => {
|
|
||||||
if (!entries.length) return;
|
|
||||||
if (entries[0].isIntersecting) {
|
|
||||||
this.style.height = "";
|
|
||||||
if (this._detachedChild) {
|
|
||||||
this.appendChild(this._detachedChild);
|
|
||||||
this._detachedChild = null;
|
|
||||||
}
|
|
||||||
this._updateCard(this.cardData); // Don't use 'newData' as it might have changed.
|
|
||||||
this._updatesAllowed = true;
|
|
||||||
} else {
|
|
||||||
// Set the card to be 48px high. Otherwise if the card is kept as 0px height then all
|
|
||||||
// following cards would trigger the observer at once.
|
|
||||||
const offsetHeight = this.offsetHeight;
|
|
||||||
this.style.height = `${offsetHeight || 48}px`;
|
|
||||||
if (this.lastChild) {
|
|
||||||
this._detachedChild = this.lastChild;
|
|
||||||
this.removeChild(this.lastChild);
|
|
||||||
}
|
|
||||||
this._updatesAllowed = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.observer.observe(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
cardDataChanged(newData) {
|
|
||||||
if (!newData) return;
|
|
||||||
// ha-entities-card is exempt from observer as it doesn't load heavy resources.
|
|
||||||
// and usually doesn't load external resources (except for entity_picture).
|
|
||||||
const eligibleToObserver =
|
|
||||||
window.IntersectionObserver && newData.cardType !== "entities";
|
|
||||||
if (!eligibleToObserver) {
|
|
||||||
if (this.observer) {
|
|
||||||
this.observer.unobserve(this);
|
|
||||||
this.observer = null;
|
|
||||||
}
|
|
||||||
this.style.height = "";
|
|
||||||
this._updateCard(newData);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.observer) {
|
|
||||||
this.createObserver();
|
|
||||||
}
|
|
||||||
if (this._updatesAllowed) {
|
|
||||||
this._updateCard(newData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define("ha-card-chooser", HaCardChooser);
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
|
|
||||||
import "../components/entity/ha-entity-toggle";
|
|
||||||
import "../components/ha-card";
|
|
||||||
import "../state-summary/state-card-content";
|
|
||||||
|
|
||||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
|
||||||
import { computeStateName } from "../common/entity/compute_state_name";
|
|
||||||
import { stateMoreInfoType } from "../common/entity/state_more_info_type";
|
|
||||||
import { canToggleState } from "../common/entity/can_toggle_state";
|
|
||||||
import { EventsMixin } from "../mixins/events-mixin";
|
|
||||||
import LocalizeMixin from "../mixins/localize-mixin";
|
|
||||||
|
|
||||||
class HaEntitiesCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style include="iron-flex"></style>
|
|
||||||
<style>
|
|
||||||
ha-card {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
.states {
|
|
||||||
margin: -4px 0;
|
|
||||||
}
|
|
||||||
.state {
|
|
||||||
padding: 4px 0;
|
|
||||||
}
|
|
||||||
.header {
|
|
||||||
@apply --paper-font-headline;
|
|
||||||
/* overwriting line-height +8 because entity-toggle can be 40px height,
|
|
||||||
compensating this with reduced padding */
|
|
||||||
line-height: 40px;
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
padding: 4px 0 12px;
|
|
||||||
}
|
|
||||||
.header .name {
|
|
||||||
@apply --paper-font-common-nowrap;
|
|
||||||
}
|
|
||||||
ha-entity-toggle {
|
|
||||||
margin-left: 16px;
|
|
||||||
}
|
|
||||||
.more-info {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<ha-card>
|
|
||||||
<template is="dom-if" if="[[title]]">
|
|
||||||
<div
|
|
||||||
class$="[[computeTitleClass(groupEntity)]]"
|
|
||||||
on-click="entityTapped"
|
|
||||||
>
|
|
||||||
<div class="flex name">[[title]]</div>
|
|
||||||
<template is="dom-if" if="[[showGroupToggle(groupEntity, states)]]">
|
|
||||||
<ha-entity-toggle
|
|
||||||
hass="[[hass]]"
|
|
||||||
state-obj="[[groupEntity]]"
|
|
||||||
></ha-entity-toggle>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="states">
|
|
||||||
<template
|
|
||||||
is="dom-repeat"
|
|
||||||
items="[[states]]"
|
|
||||||
on-dom-change="addTapEvents"
|
|
||||||
>
|
|
||||||
<div class$="[[computeStateClass(item)]]">
|
|
||||||
<state-card-content
|
|
||||||
hass="[[hass]]"
|
|
||||||
class="state-card"
|
|
||||||
state-obj="[[item]]"
|
|
||||||
></state-card-content>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: Object,
|
|
||||||
states: Array,
|
|
||||||
groupEntity: Object,
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
computed: "computeTitle(states, groupEntity, localize)",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
// We need to save a single bound function reference to ensure the event listener
|
|
||||||
// can identify it properly.
|
|
||||||
this.entityTapped = this.entityTapped.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
computeTitle(states, groupEntity, localize) {
|
|
||||||
if (groupEntity) {
|
|
||||||
return computeStateName(groupEntity).trim();
|
|
||||||
}
|
|
||||||
const domain = computeStateDomain(states[0]);
|
|
||||||
return (
|
|
||||||
(localize && localize(`domain.${domain}`)) || domain.replace(/_/g, " ")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
computeTitleClass(groupEntity) {
|
|
||||||
let classes = "header horizontal layout center ";
|
|
||||||
if (groupEntity) {
|
|
||||||
classes += "more-info";
|
|
||||||
}
|
|
||||||
return classes;
|
|
||||||
}
|
|
||||||
|
|
||||||
computeStateClass(stateObj) {
|
|
||||||
return stateMoreInfoType(stateObj) !== "hidden"
|
|
||||||
? "state more-info"
|
|
||||||
: "state";
|
|
||||||
}
|
|
||||||
|
|
||||||
addTapEvents() {
|
|
||||||
const cards = this.root.querySelectorAll(".state");
|
|
||||||
cards.forEach((card) => {
|
|
||||||
if (card.classList.contains("more-info")) {
|
|
||||||
card.addEventListener("click", this.entityTapped);
|
|
||||||
} else {
|
|
||||||
card.removeEventListener("click", this.entityTapped);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
entityTapped(ev) {
|
|
||||||
const item = this.root
|
|
||||||
.querySelector("dom-repeat")
|
|
||||||
.itemForElement(ev.target);
|
|
||||||
let entityId;
|
|
||||||
if (!item && !this.groupEntity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ev.stopPropagation();
|
|
||||||
|
|
||||||
if (item) {
|
|
||||||
entityId = item.entity_id;
|
|
||||||
} else {
|
|
||||||
entityId = this.groupEntity.entity_id;
|
|
||||||
}
|
|
||||||
this.fire("hass-more-info", { entityId: entityId });
|
|
||||||
}
|
|
||||||
|
|
||||||
showGroupToggle(groupEntity, states) {
|
|
||||||
if (
|
|
||||||
!groupEntity ||
|
|
||||||
!states ||
|
|
||||||
groupEntity.attributes.control === "hidden" ||
|
|
||||||
(groupEntity.state !== "on" && groupEntity.state !== "off")
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only show if we can toggle 2+ entities in group
|
|
||||||
let canToggleCount = 0;
|
|
||||||
for (let i = 0; i < states.length; i++) {
|
|
||||||
if (!canToggleState(this.hass, states[i])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
canToggleCount++;
|
|
||||||
|
|
||||||
if (canToggleCount > 1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return canToggleCount > 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define("ha-entities-card", HaEntitiesCard);
|
|
||||||
@@ -1,409 +0,0 @@
|
|||||||
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
|
||||||
import "@polymer/paper-icon-button/paper-icon-button";
|
|
||||||
import "@polymer/paper-progress/paper-progress";
|
|
||||||
import "@polymer/paper-styles/element-styles/paper-material-styles";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
|
|
||||||
import HassMediaPlayerEntity from "../util/hass-media-player-model";
|
|
||||||
import { fetchMediaPlayerThumbnailWithCache } from "../data/media-player";
|
|
||||||
|
|
||||||
import { computeStateName } from "../common/entity/compute_state_name";
|
|
||||||
import { EventsMixin } from "../mixins/events-mixin";
|
|
||||||
import LocalizeMixin from "../mixins/localize-mixin";
|
|
||||||
|
|
||||||
/*
|
|
||||||
* @appliesMixin LocalizeMixin
|
|
||||||
* @appliesMixin EventsMixin
|
|
||||||
*/
|
|
||||||
class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style
|
|
||||||
include="paper-material-styles iron-flex iron-flex-alignment iron-positioning"
|
|
||||||
>
|
|
||||||
:host {
|
|
||||||
@apply --paper-material-elevation-1;
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
font-size: 0px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner {
|
|
||||||
position: relative;
|
|
||||||
background-color: white;
|
|
||||||
border-top-left-radius: 2px;
|
|
||||||
border-top-right-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner:before {
|
|
||||||
display: block;
|
|
||||||
content: "";
|
|
||||||
width: 100%;
|
|
||||||
/* removed .25% from 16:9 ratio to fix YT black bars */
|
|
||||||
padding-top: 56%;
|
|
||||||
transition: padding-top 0.8s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner.no-cover {
|
|
||||||
background-position: center center;
|
|
||||||
background-image: url(/static/images/card_media_player_bg.png);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner.content-type-music:before {
|
|
||||||
padding-top: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner.content-type-game:before {
|
|
||||||
padding-top: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner.no-cover:before {
|
|
||||||
padding-top: 88px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner > .cover {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
|
|
||||||
border-top-left-radius: 2px;
|
|
||||||
border-top-right-radius: 2px;
|
|
||||||
|
|
||||||
background-position: center center;
|
|
||||||
background-size: cover;
|
|
||||||
transition: opacity 0.8s;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner.is-off > .cover {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner > .caption {
|
|
||||||
@apply --paper-font-caption;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
|
|
||||||
background-color: rgba(0, 0, 0, var(--dark-secondary-opacity));
|
|
||||||
|
|
||||||
padding: 8px 16px;
|
|
||||||
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
transition: background-color 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner.is-off > .caption {
|
|
||||||
background-color: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner > .caption .title {
|
|
||||||
@apply --paper-font-common-nowrap;
|
|
||||||
font-size: 1.2em;
|
|
||||||
margin: 8px 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress {
|
|
||||||
width: 100%;
|
|
||||||
height: var(--paper-progress-height, 4px);
|
|
||||||
margin-top: calc(-1 * var(--paper-progress-height, 4px));
|
|
||||||
--paper-progress-active-color: var(--accent-color);
|
|
||||||
--paper-progress-container-color: rgba(200, 200, 200, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
position: relative;
|
|
||||||
@apply --paper-font-body1;
|
|
||||||
padding: 8px;
|
|
||||||
border-bottom-left-radius: 2px;
|
|
||||||
border-bottom-right-radius: 2px;
|
|
||||||
background-color: var(--paper-card-background-color, white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls paper-icon-button {
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playback-controls {
|
|
||||||
direction: ltr;
|
|
||||||
}
|
|
||||||
|
|
||||||
paper-icon-button {
|
|
||||||
opacity: var(--dark-primary-opacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
paper-icon-button[disabled] {
|
|
||||||
opacity: var(--dark-disabled-opacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
paper-icon-button.primary {
|
|
||||||
width: 56px !important;
|
|
||||||
height: 56px !important;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
border-radius: 50%;
|
|
||||||
padding: 8px;
|
|
||||||
transition: background-color 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
paper-icon-button.primary[disabled] {
|
|
||||||
background-color: rgba(0, 0, 0, var(--dark-disabled-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
[invisible] {
|
|
||||||
visibility: hidden !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class$="[[computeBannerClasses(playerObj, _coverShowing, _coverLoadError)]]"
|
|
||||||
>
|
|
||||||
<div class="cover" id="cover"></div>
|
|
||||||
|
|
||||||
<div class="caption">
|
|
||||||
[[_computeStateName(stateObj)]]
|
|
||||||
<div class="title">[[computePrimaryText(localize, playerObj)]]</div>
|
|
||||||
[[playerObj.secondaryTitle]]<br />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<paper-progress
|
|
||||||
max="[[stateObj.attributes.media_duration]]"
|
|
||||||
value="[[playbackPosition]]"
|
|
||||||
hidden$="[[computeHideProgress(playerObj)]]"
|
|
||||||
class="progress"
|
|
||||||
></paper-progress>
|
|
||||||
|
|
||||||
<div class="controls layout horizontal justified">
|
|
||||||
<paper-icon-button
|
|
||||||
aria-label="Turn off"
|
|
||||||
icon="hass:power"
|
|
||||||
on-click="handleTogglePower"
|
|
||||||
invisible$="[[computeHidePowerButton(playerObj)]]"
|
|
||||||
class="self-center secondary"
|
|
||||||
></paper-icon-button>
|
|
||||||
|
|
||||||
<div class="playback-controls">
|
|
||||||
<paper-icon-button
|
|
||||||
aria-label="Previous track"
|
|
||||||
icon="hass:skip-previous"
|
|
||||||
invisible$="[[!playerObj.supportsPreviousTrack]]"
|
|
||||||
disabled="[[playerObj.isOff]]"
|
|
||||||
on-click="handlePrevious"
|
|
||||||
></paper-icon-button>
|
|
||||||
<paper-icon-button
|
|
||||||
aria-label="Play or Pause"
|
|
||||||
class="primary"
|
|
||||||
icon="[[computePlaybackControlIcon(playerObj)]]"
|
|
||||||
invisible$="[[!computePlaybackControlIcon(playerObj)]]"
|
|
||||||
disabled="[[playerObj.isOff]]"
|
|
||||||
on-click="handlePlaybackControl"
|
|
||||||
></paper-icon-button>
|
|
||||||
<paper-icon-button
|
|
||||||
aria-label="Next track"
|
|
||||||
icon="hass:skip-next"
|
|
||||||
invisible$="[[!playerObj.supportsNextTrack]]"
|
|
||||||
disabled="[[playerObj.isOff]]"
|
|
||||||
on-click="handleNext"
|
|
||||||
></paper-icon-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<paper-icon-button
|
|
||||||
aria-label="More information."
|
|
||||||
icon="hass:dots-vertical"
|
|
||||||
on-click="handleOpenMoreInfo"
|
|
||||||
class="self-center secondary"
|
|
||||||
></paper-icon-button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: Object,
|
|
||||||
stateObj: Object,
|
|
||||||
playerObj: {
|
|
||||||
type: Object,
|
|
||||||
computed: "computePlayerObj(hass, stateObj)",
|
|
||||||
observer: "playerObjChanged",
|
|
||||||
},
|
|
||||||
playbackControlIcon: {
|
|
||||||
type: String,
|
|
||||||
computed: "computePlaybackControlIcon(playerObj)",
|
|
||||||
},
|
|
||||||
playbackPosition: Number,
|
|
||||||
_coverShowing: {
|
|
||||||
type: Boolean,
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
_coverLoadError: {
|
|
||||||
type: Boolean,
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async playerObjChanged(playerObj, oldPlayerObj) {
|
|
||||||
if (playerObj.isPlaying && playerObj.showProgress) {
|
|
||||||
if (!this._positionTracking) {
|
|
||||||
this._positionTracking = setInterval(
|
|
||||||
() => this.updatePlaybackPosition(),
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (this._positionTracking) {
|
|
||||||
clearInterval(this._positionTracking);
|
|
||||||
this._positionTracking = null;
|
|
||||||
}
|
|
||||||
if (playerObj.showProgress) {
|
|
||||||
this.updatePlaybackPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
const picture = playerObj.stateObj.attributes.entity_picture;
|
|
||||||
const oldPicture =
|
|
||||||
oldPlayerObj && oldPlayerObj.stateObj.attributes.entity_picture;
|
|
||||||
|
|
||||||
if (picture !== oldPicture && !picture) {
|
|
||||||
this.$.cover.style.backgroundImage = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (picture === oldPicture) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have a new picture url
|
|
||||||
// If entity picture is non-relative, we use that url directly.
|
|
||||||
if (picture.substr(0, 1) !== "/") {
|
|
||||||
this._coverShowing = true;
|
|
||||||
this._coverLoadError = false;
|
|
||||||
this.$.cover.style.backgroundImage = `url(${picture})`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
content_type: contentType,
|
|
||||||
content,
|
|
||||||
} = await fetchMediaPlayerThumbnailWithCache(
|
|
||||||
this.hass,
|
|
||||||
playerObj.stateObj.entity_id
|
|
||||||
);
|
|
||||||
this._coverShowing = true;
|
|
||||||
this._coverLoadError = false;
|
|
||||||
this.$.cover.style.backgroundImage = `url(data:${contentType};base64,${content})`;
|
|
||||||
} catch (err) {
|
|
||||||
this._coverShowing = false;
|
|
||||||
this._coverLoadError = true;
|
|
||||||
this.$.cover.style.backgroundImage = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePlaybackPosition() {
|
|
||||||
this.playbackPosition = this.playerObj.currentProgress;
|
|
||||||
}
|
|
||||||
|
|
||||||
computeBannerClasses(playerObj, coverShowing, coverLoadError) {
|
|
||||||
var cls = "banner";
|
|
||||||
|
|
||||||
if (!playerObj) {
|
|
||||||
return cls;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (playerObj.isOff || playerObj.isIdle) {
|
|
||||||
cls += " is-off no-cover";
|
|
||||||
} else if (
|
|
||||||
!playerObj.stateObj.attributes.entity_picture ||
|
|
||||||
coverLoadError ||
|
|
||||||
!coverShowing
|
|
||||||
) {
|
|
||||||
cls += " no-cover";
|
|
||||||
} else if (playerObj.stateObj.attributes.media_content_type === "music") {
|
|
||||||
cls += " content-type-music";
|
|
||||||
} else if (playerObj.stateObj.attributes.media_content_type === "game") {
|
|
||||||
cls += " content-type-game";
|
|
||||||
}
|
|
||||||
return cls;
|
|
||||||
}
|
|
||||||
|
|
||||||
computeHideProgress(playerObj) {
|
|
||||||
return !playerObj.showProgress;
|
|
||||||
}
|
|
||||||
|
|
||||||
computeHidePowerButton(playerObj) {
|
|
||||||
return playerObj.isOff
|
|
||||||
? !playerObj.supportsTurnOn
|
|
||||||
: !playerObj.supportsTurnOff;
|
|
||||||
}
|
|
||||||
|
|
||||||
computePlayerObj(hass, stateObj) {
|
|
||||||
return new HassMediaPlayerEntity(hass, stateObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
computePrimaryText(localize, playerObj) {
|
|
||||||
return (
|
|
||||||
playerObj.primaryTitle ||
|
|
||||||
localize(`state.media_player.${playerObj.stateObj.state}`) ||
|
|
||||||
localize(`state.default.${playerObj.stateObj.state}`) ||
|
|
||||||
playerObj.stateObj.state
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
computePlaybackControlIcon(playerObj) {
|
|
||||||
if (playerObj.isPlaying) {
|
|
||||||
return playerObj.supportsPause ? "hass:pause" : "hass:stop";
|
|
||||||
}
|
|
||||||
if (playerObj.hasMediaControl || playerObj.isOff || playerObj.isIdle) {
|
|
||||||
if (
|
|
||||||
playerObj.hasMediaControl &&
|
|
||||||
playerObj.supportsPause &&
|
|
||||||
!playerObj.isPaused
|
|
||||||
) {
|
|
||||||
return "hass:play-pause";
|
|
||||||
}
|
|
||||||
return playerObj.supportsPlay ? "hass:play" : null;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeStateName(stateObj) {
|
|
||||||
return computeStateName(stateObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNext(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
this.playerObj.nextTrack();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleOpenMoreInfo(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
this.fire("hass-more-info", { entityId: this.stateObj.entity_id });
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePlaybackControl(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
this.playerObj.mediaPlayPause();
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePrevious(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
this.playerObj.previousTrack();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTogglePower(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
this.playerObj.togglePower();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define("ha-media_player-card", HaMediaPlayerCard);
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import "@material/mwc-button";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
|
|
||||||
import "../components/ha-card";
|
|
||||||
import "../components/ha-markdown";
|
|
||||||
|
|
||||||
import { computeStateName } from "../common/entity/compute_state_name";
|
|
||||||
import LocalizeMixin from "../mixins/localize-mixin";
|
|
||||||
import { computeObjectId } from "../common/entity/compute_object_id";
|
|
||||||
|
|
||||||
/*
|
|
||||||
* @appliesMixin LocalizeMixin
|
|
||||||
*/
|
|
||||||
class HaPersistentNotificationCard extends LocalizeMixin(PolymerElement) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
:host {
|
|
||||||
@apply --paper-font-body1;
|
|
||||||
}
|
|
||||||
ha-markdown {
|
|
||||||
display: block;
|
|
||||||
padding: 0 16px;
|
|
||||||
-ms-user-select: initial;
|
|
||||||
-webkit-user-select: initial;
|
|
||||||
-moz-user-select: initial;
|
|
||||||
}
|
|
||||||
ha-markdown p:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
ha-markdown p:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
ha-markdown a {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
ha-markdown img {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
mwc-button {
|
|
||||||
margin: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<ha-card header="[[computeTitle(stateObj)]]">
|
|
||||||
<ha-markdown content="[[stateObj.attributes.message]]"></ha-markdown>
|
|
||||||
<mwc-button on-click="dismissTap"
|
|
||||||
>[[localize('ui.card.persistent_notification.dismiss')]]</mwc-button
|
|
||||||
>
|
|
||||||
</ha-card>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: Object,
|
|
||||||
stateObj: Object,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
computeTitle(stateObj) {
|
|
||||||
return stateObj.attributes.title || computeStateName(stateObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
dismissTap(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
this.hass.callService("persistent_notification", "dismiss", {
|
|
||||||
notification_id: computeObjectId(this.stateObj.entity_id),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define(
|
|
||||||
"ha-persistent_notification-card",
|
|
||||||
HaPersistentNotificationCard
|
|
||||||
);
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
|
|
||||||
import "../components/ha-card";
|
|
||||||
import "../components/ha-icon";
|
|
||||||
|
|
||||||
import { computeStateName } from "../common/entity/compute_state_name";
|
|
||||||
import { EventsMixin } from "../mixins/events-mixin";
|
|
||||||
|
|
||||||
class HaPlantCard extends EventsMixin(PolymerElement) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
.banner {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
padding-top: 12px;
|
|
||||||
}
|
|
||||||
.has-plant-image .banner {
|
|
||||||
padding-top: 30%;
|
|
||||||
}
|
|
||||||
.header {
|
|
||||||
@apply --paper-font-headline;
|
|
||||||
line-height: 40px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
}
|
|
||||||
.has-plant-image .header {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 16px;
|
|
||||||
padding: 16px;
|
|
||||||
color: white;
|
|
||||||
width: 100%;
|
|
||||||
background: rgba(0, 0, 0, var(--dark-secondary-opacity));
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 16px 32px 24px 32px;
|
|
||||||
}
|
|
||||||
.has-plant-image .content {
|
|
||||||
padding-bottom: 16px;
|
|
||||||
}
|
|
||||||
ha-icon {
|
|
||||||
color: var(--paper-item-icon-color);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.attributes {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.attributes div {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.problem {
|
|
||||||
color: var(--google-red-500);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.uom {
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<ha-card
|
|
||||||
class$="[[computeImageClass(stateObj.attributes.entity_picture)]]"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="banner"
|
|
||||||
style="background-image:url([[stateObj.attributes.entity_picture]])"
|
|
||||||
>
|
|
||||||
<div class="header">[[computeTitle(stateObj)]]</div>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<template
|
|
||||||
is="dom-repeat"
|
|
||||||
items="[[computeAttributes(stateObj.attributes)]]"
|
|
||||||
>
|
|
||||||
<div class="attributes" on-click="attributeClicked">
|
|
||||||
<div>
|
|
||||||
<ha-icon
|
|
||||||
icon="[[computeIcon(item, stateObj.attributes.battery)]]"
|
|
||||||
></ha-icon>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class$="[[computeAttributeClass(stateObj.attributes.problem, item)]]"
|
|
||||||
>
|
|
||||||
[[computeValue(stateObj.attributes, item)]]
|
|
||||||
</div>
|
|
||||||
<div class="uom">
|
|
||||||
[[computeUom(stateObj.attributes.unit_of_measurement_dict,
|
|
||||||
item)]]
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: Object,
|
|
||||||
stateObj: Object,
|
|
||||||
config: Object,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.sensors = {
|
|
||||||
moisture: "hass:water",
|
|
||||||
temperature: "hass:thermometer",
|
|
||||||
brightness: "hass:white-balance-sunny",
|
|
||||||
conductivity: "hass:emoticon-poop",
|
|
||||||
battery: "hass:battery",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
computeTitle(stateObj) {
|
|
||||||
return (this.config && this.config.name) || computeStateName(stateObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
computeAttributes(data) {
|
|
||||||
return Object.keys(this.sensors).filter((key) => key in data);
|
|
||||||
}
|
|
||||||
|
|
||||||
computeIcon(attr, batLvl) {
|
|
||||||
const icon = this.sensors[attr];
|
|
||||||
if (attr === "battery") {
|
|
||||||
if (batLvl <= 5) {
|
|
||||||
return `${icon}-alert`;
|
|
||||||
}
|
|
||||||
if (batLvl < 95) {
|
|
||||||
return `${icon}-${Math.round(batLvl / 10 - 0.01) * 10}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
computeValue(attributes, attr) {
|
|
||||||
return attributes[attr];
|
|
||||||
}
|
|
||||||
|
|
||||||
computeUom(dict, attr) {
|
|
||||||
return dict[attr] || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
computeAttributeClass(problem, attr) {
|
|
||||||
return problem.indexOf(attr) === -1 ? "" : "problem";
|
|
||||||
}
|
|
||||||
|
|
||||||
computeImageClass(entityPicture) {
|
|
||||||
return entityPicture ? "has-plant-image" : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
attributeClicked(ev) {
|
|
||||||
this.fire("hass-more-info", {
|
|
||||||
entityId: this.stateObj.attributes.sensors[ev.model.item],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("ha-plant-card", HaPlantCard);
|
|
||||||
@@ -1,383 +0,0 @@
|
|||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
|
|
||||||
import { computeStateName } from "../common/entity/compute_state_name";
|
|
||||||
|
|
||||||
import "../components/ha-card";
|
|
||||||
import "../components/ha-icon";
|
|
||||||
|
|
||||||
import { EventsMixin } from "../mixins/events-mixin";
|
|
||||||
import LocalizeMixin from "../mixins/localize-mixin";
|
|
||||||
import { computeRTL } from "../common/util/compute_rtl";
|
|
||||||
|
|
||||||
/*
|
|
||||||
* @appliesMixin LocalizeMixin
|
|
||||||
*/
|
|
||||||
class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
:host {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: 0 20px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-icon {
|
|
||||||
color: var(--paper-item-icon-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
font-family: var(--paper-font-headline_-_font-family);
|
|
||||||
-webkit-font-smoothing: var(
|
|
||||||
--paper-font-headline_-_-webkit-font-smoothing
|
|
||||||
);
|
|
||||||
font-size: var(--paper-font-headline_-_font-size);
|
|
||||||
font-weight: var(--paper-font-headline_-_font-weight);
|
|
||||||
letter-spacing: var(--paper-font-headline_-_letter-spacing);
|
|
||||||
line-height: var(--paper-font-headline_-_line-height);
|
|
||||||
text-rendering: var(
|
|
||||||
--paper-font-common-expensive-kerning_-_text-rendering
|
|
||||||
);
|
|
||||||
opacity: var(--dark-primary-opacity);
|
|
||||||
padding: 24px 16px 16px;
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
margin-left: 16px;
|
|
||||||
font-size: 16px;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([rtl]) .name {
|
|
||||||
margin-left: 0px;
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.now {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-right: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([rtl]) .main {
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main ha-icon {
|
|
||||||
--iron-icon-height: 72px;
|
|
||||||
--iron-icon-width: 72px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([rtl]) .main ha-icon {
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main .temp {
|
|
||||||
font-size: 52px;
|
|
||||||
line-height: 1em;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([rtl]) .main .temp {
|
|
||||||
direction: ltr;
|
|
||||||
margin-right: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main .temp span {
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 1em;
|
|
||||||
position: absolute;
|
|
||||||
top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.measurand {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([rtl]) .measurand {
|
|
||||||
direction: ltr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.forecast {
|
|
||||||
margin-top: 16px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.forecast div {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.forecast .icon {
|
|
||||||
margin: 4px 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([rtl]) .forecast .temp {
|
|
||||||
direction: ltr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.weekday {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attributes,
|
|
||||||
.templow,
|
|
||||||
.precipitation {
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([rtl]) .precipitation {
|
|
||||||
direction: ltr;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<ha-card>
|
|
||||||
<div class="header">
|
|
||||||
[[computeState(stateObj.state, localize)]]
|
|
||||||
<div class="name">[[computeName(stateObj)]]</div>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<div class="now">
|
|
||||||
<div class="main">
|
|
||||||
<template is="dom-if" if="[[showWeatherIcon(stateObj.state)]]">
|
|
||||||
<ha-icon icon="[[getWeatherIcon(stateObj.state)]]"></ha-icon>
|
|
||||||
</template>
|
|
||||||
<div class="temp">
|
|
||||||
[[stateObj.attributes.temperature]]<span
|
|
||||||
>[[getUnit('temperature')]]</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="attributes">
|
|
||||||
<template
|
|
||||||
is="dom-if"
|
|
||||||
if="[[_showValue(stateObj.attributes.pressure)]]"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
[[localize('ui.card.weather.attributes.air_pressure')]]:
|
|
||||||
<span class="measurand">
|
|
||||||
[[stateObj.attributes.pressure]] [[getUnit('air_pressure')]]
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
is="dom-if"
|
|
||||||
if="[[_showValue(stateObj.attributes.humidity)]]"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
[[localize('ui.card.weather.attributes.humidity')]]:
|
|
||||||
<span class="measurand"
|
|
||||||
>[[stateObj.attributes.humidity]] %</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
is="dom-if"
|
|
||||||
if="[[_showValue(stateObj.attributes.wind_speed)]]"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
[[localize('ui.card.weather.attributes.wind_speed')]]:
|
|
||||||
<span class="measurand">
|
|
||||||
[[getWindSpeed(stateObj.attributes.wind_speed)]]
|
|
||||||
</span>
|
|
||||||
[[getWindBearing(stateObj.attributes.wind_bearing, localize)]]
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template is="dom-if" if="[[forecast]]">
|
|
||||||
<div class="forecast">
|
|
||||||
<template is="dom-repeat" items="[[forecast]]">
|
|
||||||
<div>
|
|
||||||
<div class="weekday">
|
|
||||||
[[computeDate(item.datetime)]]<br />
|
|
||||||
<template is="dom-if" if="[[!_showValue(item.templow)]]">
|
|
||||||
[[computeTime(item.datetime)]]
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<template is="dom-if" if="[[_showValue(item.condition)]]">
|
|
||||||
<div class="icon">
|
|
||||||
<ha-icon
|
|
||||||
icon="[[getWeatherIcon(item.condition)]]"
|
|
||||||
></ha-icon>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[_showValue(item.temperature)]]">
|
|
||||||
<div class="temp">
|
|
||||||
[[item.temperature]] [[getUnit('temperature')]]
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[_showValue(item.templow)]]">
|
|
||||||
<div class="templow">
|
|
||||||
[[item.templow]] [[getUnit('temperature')]]
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[_showValue(item.precipitation)]]">
|
|
||||||
<div class="precipitation">
|
|
||||||
[[item.precipitation]] [[getUnit('precipitation')]]
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: Object,
|
|
||||||
config: Object,
|
|
||||||
stateObj: Object,
|
|
||||||
forecast: {
|
|
||||||
type: Array,
|
|
||||||
computed: "computeForecast(stateObj.attributes.forecast)",
|
|
||||||
},
|
|
||||||
rtl: {
|
|
||||||
type: Boolean,
|
|
||||||
reflectToAttribute: true,
|
|
||||||
computed: "_computeRTL(hass)",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.cardinalDirections = [
|
|
||||||
"N",
|
|
||||||
"NNE",
|
|
||||||
"NE",
|
|
||||||
"ENE",
|
|
||||||
"E",
|
|
||||||
"ESE",
|
|
||||||
"SE",
|
|
||||||
"SSE",
|
|
||||||
"S",
|
|
||||||
"SSW",
|
|
||||||
"SW",
|
|
||||||
"WSW",
|
|
||||||
"W",
|
|
||||||
"WNW",
|
|
||||||
"NW",
|
|
||||||
"NNW",
|
|
||||||
"N",
|
|
||||||
];
|
|
||||||
this.weatherIcons = {
|
|
||||||
"clear-night": "hass:weather-night",
|
|
||||||
cloudy: "hass:weather-cloudy",
|
|
||||||
exceptional: "hass:alert-circle-outline",
|
|
||||||
fog: "hass:weather-fog",
|
|
||||||
hail: "hass:weather-hail",
|
|
||||||
lightning: "hass:weather-lightning",
|
|
||||||
"lightning-rainy": "hass:weather-lightning-rainy",
|
|
||||||
partlycloudy: "hass:weather-partly-cloudy",
|
|
||||||
pouring: "hass:weather-pouring",
|
|
||||||
rainy: "hass:weather-rainy",
|
|
||||||
snowy: "hass:weather-snowy",
|
|
||||||
"snowy-rainy": "hass:weather-snowy-rainy",
|
|
||||||
sunny: "hass:weather-sunny",
|
|
||||||
windy: "hass:weather-windy",
|
|
||||||
"windy-variant": "hass:weather-windy-variant",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ready() {
|
|
||||||
this.addEventListener("click", this._onClick);
|
|
||||||
super.ready();
|
|
||||||
}
|
|
||||||
|
|
||||||
_onClick() {
|
|
||||||
this.fire("hass-more-info", { entityId: this.stateObj.entity_id });
|
|
||||||
}
|
|
||||||
|
|
||||||
computeForecast(forecast) {
|
|
||||||
return forecast && forecast.slice(0, 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
getUnit(measure) {
|
|
||||||
const lengthUnit = this.hass.config.unit_system.length || "";
|
|
||||||
switch (measure) {
|
|
||||||
case "air_pressure":
|
|
||||||
return lengthUnit === "km" ? "hPa" : "inHg";
|
|
||||||
case "length":
|
|
||||||
return lengthUnit;
|
|
||||||
case "precipitation":
|
|
||||||
return lengthUnit === "km" ? "mm" : "in";
|
|
||||||
default:
|
|
||||||
return this.hass.config.unit_system[measure] || "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
computeState(state, localize) {
|
|
||||||
return localize(`state.weather.${state}`) || state;
|
|
||||||
}
|
|
||||||
|
|
||||||
computeName(stateObj) {
|
|
||||||
return (this.config && this.config.name) || computeStateName(stateObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
showWeatherIcon(condition) {
|
|
||||||
return condition in this.weatherIcons;
|
|
||||||
}
|
|
||||||
|
|
||||||
getWeatherIcon(condition) {
|
|
||||||
return this.weatherIcons[condition];
|
|
||||||
}
|
|
||||||
|
|
||||||
windBearingToText(degree) {
|
|
||||||
const degreenum = parseInt(degree);
|
|
||||||
if (isFinite(degreenum)) {
|
|
||||||
return this.cardinalDirections[(((degreenum + 11.25) / 22.5) | 0) % 16];
|
|
||||||
}
|
|
||||||
return degree;
|
|
||||||
}
|
|
||||||
|
|
||||||
getWindSpeed(speed) {
|
|
||||||
return `${speed} ${this.getUnit("length")}/h`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getWindBearing(bearing, localize) {
|
|
||||||
if (bearing != null) {
|
|
||||||
const cardinalDirection = this.windBearingToText(bearing);
|
|
||||||
return `(${localize(
|
|
||||||
`ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}`
|
|
||||||
) || cardinalDirection})`;
|
|
||||||
}
|
|
||||||
return ``;
|
|
||||||
}
|
|
||||||
|
|
||||||
_showValue(item) {
|
|
||||||
return typeof item !== "undefined" && item !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
computeDate(data) {
|
|
||||||
const date = new Date(data);
|
|
||||||
return date.toLocaleDateString(this.hass.language, { weekday: "short" });
|
|
||||||
}
|
|
||||||
|
|
||||||
computeTime(data) {
|
|
||||||
const date = new Date(data);
|
|
||||||
return date.toLocaleTimeString(this.hass.language, { hour: "numeric" });
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeRTL(hass) {
|
|
||||||
return computeRTL(hass);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define("ha-weather-card", HaWeatherCard);
|
|
||||||
@@ -21,6 +21,7 @@ export interface ConnectMessage extends BaseCastMessage {
|
|||||||
export interface ShowLovelaceViewMessage extends BaseCastMessage {
|
export interface ShowLovelaceViewMessage extends BaseCastMessage {
|
||||||
type: "show_lovelace_view";
|
type: "show_lovelace_view";
|
||||||
viewPath: string | number | null;
|
viewPath: string | number | null;
|
||||||
|
urlPath: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShowDemoMessage extends BaseCastMessage {
|
export interface ShowDemoMessage extends BaseCastMessage {
|
||||||
@@ -43,11 +44,13 @@ export const castSendAuth = (cast: CastManager, auth: Auth) =>
|
|||||||
|
|
||||||
export const castSendShowLovelaceView = (
|
export const castSendShowLovelaceView = (
|
||||||
cast: CastManager,
|
cast: CastManager,
|
||||||
viewPath: ShowLovelaceViewMessage["viewPath"]
|
viewPath: ShowLovelaceViewMessage["viewPath"],
|
||||||
|
urlPath?: string | null
|
||||||
) =>
|
) =>
|
||||||
cast.sendMessage({
|
cast.sendMessage({
|
||||||
type: "show_lovelace_view",
|
type: "show_lovelace_view",
|
||||||
viewPath,
|
viewPath,
|
||||||
|
urlPath: urlPath || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const castSendShowDemo = (cast: CastManager) =>
|
export const castSendShowDemo = (cast: CastManager) =>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface ReceiverStatusMessage extends BaseCastMessage {
|
|||||||
showDemo: boolean;
|
showDemo: boolean;
|
||||||
hassUrl?: string;
|
hassUrl?: string;
|
||||||
lovelacePath?: string | number | null;
|
lovelacePath?: string | number | null;
|
||||||
|
urlPath?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SenderMessage = ReceiverStatusMessage;
|
export type SenderMessage = ReceiverStatusMessage;
|
||||||
|
|||||||
11
src/common/config/version.ts
Normal file
11
src/common/config/version.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export const atLeastVersion = (
|
||||||
|
version: string,
|
||||||
|
major: number,
|
||||||
|
minor: number
|
||||||
|
): boolean => {
|
||||||
|
const [haMajor, haMinor] = version.split(".", 2);
|
||||||
|
return (
|
||||||
|
Number(haMajor) > major ||
|
||||||
|
(Number(haMajor) === major && Number(haMinor) >= minor)
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -44,6 +44,7 @@ export const DOMAINS_WITH_MORE_INFO = [
|
|||||||
"light",
|
"light",
|
||||||
"lock",
|
"lock",
|
||||||
"media_player",
|
"media_player",
|
||||||
|
"person",
|
||||||
"script",
|
"script",
|
||||||
"sun",
|
"sun",
|
||||||
"timer",
|
"timer",
|
||||||
|
|||||||
31
src/common/datetime/check_options_support.ts
Normal file
31
src/common/datetime/check_options_support.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Check for support of native locale string options
|
||||||
|
function checkToLocaleDateStringSupportsOptions() {
|
||||||
|
try {
|
||||||
|
new Date().toLocaleDateString("i");
|
||||||
|
} catch (e) {
|
||||||
|
return e.name === "RangeError";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkToLocaleTimeStringSupportsOptions() {
|
||||||
|
try {
|
||||||
|
new Date().toLocaleTimeString("i");
|
||||||
|
} catch (e) {
|
||||||
|
return e.name === "RangeError";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkToLocaleStringSupportsOptions() {
|
||||||
|
try {
|
||||||
|
new Date().toLocaleString("i");
|
||||||
|
} catch (e) {
|
||||||
|
return e.name === "RangeError";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toLocaleDateStringSupportsOptions = checkToLocaleDateStringSupportsOptions();
|
||||||
|
export const toLocaleTimeStringSupportsOptions = checkToLocaleTimeStringSupportsOptions();
|
||||||
|
export const toLocaleStringSupportsOptions = checkToLocaleStringSupportsOptions();
|
||||||
@@ -1,20 +1,11 @@
|
|||||||
import fecha from "fecha";
|
import fecha from "fecha";
|
||||||
|
import { toLocaleDateStringSupportsOptions } from "./check_options_support";
|
||||||
|
|
||||||
// Check for support of native locale string options
|
export const formatDate = toLocaleDateStringSupportsOptions
|
||||||
function toLocaleDateStringSupportsOptions() {
|
|
||||||
try {
|
|
||||||
new Date().toLocaleDateString("i");
|
|
||||||
} catch (e) {
|
|
||||||
return e.name === "RangeError";
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default toLocaleDateStringSupportsOptions()
|
|
||||||
? (dateObj: Date, locales: string) =>
|
? (dateObj: Date, locales: string) =>
|
||||||
dateObj.toLocaleDateString(locales, {
|
dateObj.toLocaleDateString(locales, {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
})
|
})
|
||||||
: (dateObj: Date) => fecha.format(dateObj, "mediumDate");
|
: (dateObj: Date) => fecha.format(dateObj, "longDate");
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
import fecha from "fecha";
|
import fecha from "fecha";
|
||||||
|
import { toLocaleStringSupportsOptions } from "./check_options_support";
|
||||||
|
|
||||||
// Check for support of native locale string options
|
export const formatDateTime = toLocaleStringSupportsOptions
|
||||||
function toLocaleStringSupportsOptions() {
|
|
||||||
try {
|
|
||||||
new Date().toLocaleString("i");
|
|
||||||
} catch (e) {
|
|
||||||
return e.name === "RangeError";
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default toLocaleStringSupportsOptions()
|
|
||||||
? (dateObj: Date, locales: string) =>
|
? (dateObj: Date, locales: string) =>
|
||||||
dateObj.toLocaleString(locales, {
|
dateObj.toLocaleString(locales, {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
@@ -19,4 +10,24 @@ export default toLocaleStringSupportsOptions()
|
|||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
})
|
})
|
||||||
: (dateObj: Date) => fecha.format(dateObj, "haDateTime");
|
: (dateObj: Date) =>
|
||||||
|
fecha.format(
|
||||||
|
dateObj,
|
||||||
|
`${fecha.masks.longDate}, ${fecha.masks.shortTime}`
|
||||||
|
);
|
||||||
|
|
||||||
|
export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
|
||||||
|
? (dateObj: Date, locales: string) =>
|
||||||
|
dateObj.toLocaleString(locales, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
})
|
||||||
|
: (dateObj: Date) =>
|
||||||
|
fecha.format(
|
||||||
|
dateObj,
|
||||||
|
`${fecha.masks.longDate}, ${fecha.masks.mediumTime}`
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import fecha from "fecha";
|
import fecha from "fecha";
|
||||||
|
import { toLocaleTimeStringSupportsOptions } from "./check_options_support";
|
||||||
|
|
||||||
// Check for support of native locale string options
|
export const formatTime = toLocaleTimeStringSupportsOptions
|
||||||
function toLocaleTimeStringSupportsOptions() {
|
|
||||||
try {
|
|
||||||
new Date().toLocaleTimeString("i");
|
|
||||||
} catch (e) {
|
|
||||||
return e.name === "RangeError";
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default toLocaleTimeStringSupportsOptions()
|
|
||||||
? (dateObj: Date, locales: string) =>
|
? (dateObj: Date, locales: string) =>
|
||||||
dateObj.toLocaleTimeString(locales, {
|
dateObj.toLocaleTimeString(locales, {
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
})
|
})
|
||||||
: (dateObj: Date) => fecha.format(dateObj, "shortTime");
|
: (dateObj: Date) => fecha.format(dateObj, "shortTime");
|
||||||
|
|
||||||
|
export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
|
||||||
|
? (dateObj: Date, locales: string) =>
|
||||||
|
dateObj.toLocaleTimeString(locales, {
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
})
|
||||||
|
: (dateObj: Date) => fecha.format(dateObj, "mediumTime");
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export const dynamicElement = directive(
|
|||||||
(tag: string, properties?: { [key: string]: any }) => (part: Part): void => {
|
(tag: string, properties?: { [key: string]: any }) => (part: Part): void => {
|
||||||
if (!(part instanceof NodePart)) {
|
if (!(part instanceof NodePart)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"dynamicContentDirective can only be used in content bindings"
|
"dynamicElementDirective can only be used in content bindings"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { computeStateDomain } from "./compute_state_domain";
|
import { computeStateDomain } from "./compute_state_domain";
|
||||||
import formatDateTime from "../datetime/format_date_time";
|
import { formatDateTime } from "../datetime/format_date_time";
|
||||||
import formatDate from "../datetime/format_date";
|
import { formatDate } from "../datetime/format_date";
|
||||||
import formatTime from "../datetime/format_time";
|
import { formatTime } from "../datetime/format_time";
|
||||||
import { LocalizeFunc } from "../translations/localize";
|
import { LocalizeFunc } from "../translations/localize";
|
||||||
|
|
||||||
export const computeStateDisplay = (
|
export const computeStateDisplay = (
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const fixedIcons = {
|
|||||||
homeassistant: "hass:home-assistant",
|
homeassistant: "hass:home-assistant",
|
||||||
homekit: "hass:home-automation",
|
homekit: "hass:home-automation",
|
||||||
image_processing: "hass:image-filter-frames",
|
image_processing: "hass:image-filter-frames",
|
||||||
input_boolean: "hass:drawing",
|
input_boolean: "hass:toggle-switch-outline",
|
||||||
input_datetime: "hass:calendar-clock",
|
input_datetime: "hass:calendar-clock",
|
||||||
input_number: "hass:ray-vertex",
|
input_number: "hass:ray-vertex",
|
||||||
input_select: "hass:format-list-bulleted",
|
input_select: "hass:format-list-bulleted",
|
||||||
|
|||||||
@@ -7,14 +7,18 @@ import {
|
|||||||
property,
|
property,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { fireEvent } from "../dom/fire_event";
|
import { fireEvent } from "../dom/fire_event";
|
||||||
import "@polymer/iron-icon/iron-icon";
|
|
||||||
import "@polymer/paper-input/paper-input";
|
import "@polymer/paper-input/paper-input";
|
||||||
import "@polymer/paper-icon-button/paper-icon-button";
|
import "@polymer/paper-icon-button/paper-icon-button";
|
||||||
import "@material/mwc-button";
|
import "../../components/ha-icon";
|
||||||
|
import { classMap } from "lit-html/directives/class-map";
|
||||||
|
|
||||||
@customElement("search-input")
|
@customElement("search-input")
|
||||||
class SearchInput extends LitElement {
|
class SearchInput extends LitElement {
|
||||||
@property() public filter?: string;
|
@property() public filter?: string;
|
||||||
|
@property({ type: Boolean, attribute: "no-label-float" })
|
||||||
|
public noLabelFloat? = false;
|
||||||
|
@property({ type: Boolean, attribute: "no-underline" })
|
||||||
|
public noUnderline = false;
|
||||||
|
|
||||||
public focus() {
|
public focus() {
|
||||||
this.shadowRoot!.querySelector("paper-input")!.focus();
|
this.shadowRoot!.querySelector("paper-input")!.focus();
|
||||||
@@ -22,18 +26,24 @@ class SearchInput extends LitElement {
|
|||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
|
<style>
|
||||||
|
.no-underline {
|
||||||
|
--paper-input-container-underline: {
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<div class="search-container">
|
<div class="search-container">
|
||||||
<paper-input
|
<paper-input
|
||||||
|
class=${classMap({ "no-underline": this.noUnderline })}
|
||||||
autofocus
|
autofocus
|
||||||
label="Search"
|
label="Search"
|
||||||
.value=${this.filter}
|
.value=${this.filter}
|
||||||
@value-changed=${this._filterInputChanged}
|
@value-changed=${this._filterInputChanged}
|
||||||
|
.noLabelFloat=${this.noLabelFloat}
|
||||||
>
|
>
|
||||||
<iron-icon
|
<ha-icon icon="hass:magnify" slot="prefix" class="prefix"></ha-icon>
|
||||||
icon="hass:magnify"
|
|
||||||
slot="prefix"
|
|
||||||
class="prefix"
|
|
||||||
></iron-icon>
|
|
||||||
${this.filter &&
|
${this.filter &&
|
||||||
html`
|
html`
|
||||||
<paper-icon-button
|
<paper-icon-button
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { css } from "lit-element";
|
import { css } from "lit-element";
|
||||||
|
|
||||||
export const iconColorCSS = css`
|
export const iconColorCSS = css`
|
||||||
ha-icon[data-domain="alarm_control_panel"][data-state="disarmed"],
|
|
||||||
ha-icon[data-domain="alert"][data-state="on"],
|
ha-icon[data-domain="alert"][data-state="on"],
|
||||||
ha-icon[data-domain="automation"][data-state="on"],
|
ha-icon[data-domain="automation"][data-state="on"],
|
||||||
ha-icon[data-domain="binary_sensor"][data-state="on"],
|
ha-icon[data-domain="binary_sensor"][data-state="on"],
|
||||||
@@ -30,6 +29,38 @@ export const iconColorCSS = css`
|
|||||||
color: var(--heat-color, #ff8100);
|
color: var(--heat-color, #ff8100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ha-icon[data-domain="climate"][data-state="drying"] {
|
||||||
|
color: var(--dry-color, #efbd07);
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-icon[data-domain="alarm_control_panel"] {
|
||||||
|
color: var(--alarm-color-armed, var(--label-badge-red));
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-icon[data-domain="alarm_control_panel"][data-state="disarmed"] {
|
||||||
|
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="arming"] {
|
||||||
|
color: var(--alarm-color-pending, var(--label-badge-yellow));
|
||||||
|
animation: pulse 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-icon[data-domain="alarm_control_panel"][data-state="triggered"] {
|
||||||
|
color: var(--alarm-color-triggered, var(--label-badge-red));
|
||||||
|
animation: pulse 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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(--error-state-color, #db4437);
|
color: var(--error-state-color, #db4437);
|
||||||
|
|||||||
107
src/common/util/deep-equal.ts
Normal file
107
src/common/util/deep-equal.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
// From https://github.com/epoberezkin/fast-deep-equal
|
||||||
|
// MIT License - Copyright (c) 2017 Evgeny Poberezkin
|
||||||
|
export const deepEqual = (a: any, b: any): boolean => {
|
||||||
|
if (a === b) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a && b && typeof a === "object" && typeof b === "object") {
|
||||||
|
if (a.constructor !== b.constructor) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let i: number | [any, any];
|
||||||
|
let length: number;
|
||||||
|
if (Array.isArray(a)) {
|
||||||
|
length = a.length;
|
||||||
|
if (length !== b.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (i = length; i-- !== 0; ) {
|
||||||
|
if (!deepEqual(a[i], b[i])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a instanceof Map && b instanceof Map) {
|
||||||
|
if (a.size !== b.size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (i of a.entries()) {
|
||||||
|
if (!b.has(i[0])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i of a.entries()) {
|
||||||
|
if (!deepEqual(i[1], b.get(i[0]))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a instanceof Set && b instanceof Set) {
|
||||||
|
if (a.size !== b.size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (i of a.entries()) {
|
||||||
|
if (!b.has(i[0])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
|
||||||
|
// @ts-ignore
|
||||||
|
length = a.length;
|
||||||
|
// @ts-ignore
|
||||||
|
if (length !== b.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (i = length; i-- !== 0; ) {
|
||||||
|
if (a[i] !== b[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.constructor === RegExp) {
|
||||||
|
return a.source === b.source && a.flags === b.flags;
|
||||||
|
}
|
||||||
|
if (a.valueOf !== Object.prototype.valueOf) {
|
||||||
|
return a.valueOf() === b.valueOf();
|
||||||
|
}
|
||||||
|
if (a.toString !== Object.prototype.toString) {
|
||||||
|
return a.toString() === b.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
let keys: string[];
|
||||||
|
keys = Object.keys(a);
|
||||||
|
length = keys.length;
|
||||||
|
if (length !== Object.keys(b).length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (i = length; i-- !== 0; ) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(b, keys[i])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = length; i-- !== 0; ) {
|
||||||
|
const key = keys[i];
|
||||||
|
|
||||||
|
if (!deepEqual(a[key], b[key])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// true if both NaN, false otherwise
|
||||||
|
return a !== a && b !== b;
|
||||||
|
};
|
||||||
@@ -1,27 +1,21 @@
|
|||||||
import { repeat } from "lit-html/directives/repeat";
|
|
||||||
import deepClone from "deep-clone-simple";
|
import deepClone from "deep-clone-simple";
|
||||||
|
|
||||||
import {
|
|
||||||
MDCDataTableAdapter,
|
|
||||||
MDCDataTableFoundation,
|
|
||||||
} from "@material/data-table";
|
|
||||||
|
|
||||||
import { classMap } from "lit-html/directives/class-map";
|
import { classMap } from "lit-html/directives/class-map";
|
||||||
|
|
||||||
|
import { scroll } from "lit-virtualizer";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
html,
|
html,
|
||||||
query,
|
query,
|
||||||
queryAll,
|
|
||||||
CSSResult,
|
CSSResult,
|
||||||
css,
|
css,
|
||||||
customElement,
|
customElement,
|
||||||
property,
|
property,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
|
LitElement,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
|
|
||||||
import { BaseElement } from "@material/mwc-base/base-element";
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-webpack-loader-syntax
|
// eslint-disable-next-line import/no-webpack-loader-syntax
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// tslint:disable-next-line: no-implicit-dependencies
|
// tslint:disable-next-line: no-implicit-dependencies
|
||||||
@@ -35,6 +29,8 @@ import { HaCheckbox } from "../ha-checkbox";
|
|||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { nextRender } from "../../common/util/render-status";
|
import { nextRender } from "../../common/util/render-status";
|
||||||
import { debounce } from "../../common/util/debounce";
|
import { debounce } from "../../common/util/debounce";
|
||||||
|
import { styleMap } from "lit-html/directives/style-map";
|
||||||
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// for fire event
|
// for fire event
|
||||||
@@ -50,8 +46,7 @@ export interface RowClickedEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectionChangedEvent {
|
export interface SelectionChangedEvent {
|
||||||
id: string;
|
value: string[];
|
||||||
selected: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SortingChangedEvent {
|
export interface SortingChangedEvent {
|
||||||
@@ -76,31 +71,33 @@ export interface DataTableColumnData extends DataTableSortColumnData {
|
|||||||
title: string;
|
title: string;
|
||||||
type?: "numeric" | "icon";
|
type?: "numeric" | "icon";
|
||||||
template?: <T>(data: any, row: T) => TemplateResult | string;
|
template?: <T>(data: any, row: T) => TemplateResult | string;
|
||||||
|
width?: string;
|
||||||
|
grows?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataTableRowData {
|
export interface DataTableRowData {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
|
selectable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement("ha-data-table")
|
@customElement("ha-data-table")
|
||||||
export class HaDataTable extends BaseElement {
|
export class HaDataTable extends LitElement {
|
||||||
@property({ type: Object }) public columns: DataTableColumnContainer = {};
|
@property({ type: Object }) public columns: DataTableColumnContainer = {};
|
||||||
@property({ type: Array }) public data: DataTableRowData[] = [];
|
@property({ type: Array }) public data: DataTableRowData[] = [];
|
||||||
@property({ type: Boolean }) public selectable = false;
|
@property({ type: Boolean }) public selectable = false;
|
||||||
|
@property({ type: Boolean, attribute: "auto-height" })
|
||||||
|
public autoHeight = false;
|
||||||
@property({ type: String }) public id = "id";
|
@property({ type: String }) public id = "id";
|
||||||
@property({ type: String }) public filter = "";
|
@property({ type: String }) public filter = "";
|
||||||
protected mdcFoundation!: MDCDataTableFoundation;
|
|
||||||
protected readonly mdcFoundationClass = MDCDataTableFoundation;
|
|
||||||
@query(".mdc-data-table") protected mdcRoot!: HTMLElement;
|
|
||||||
@queryAll(".mdc-data-table__row") protected rowElements!: HTMLElement[];
|
|
||||||
@property({ type: Boolean }) private _filterable = false;
|
@property({ type: Boolean }) private _filterable = false;
|
||||||
@property({ type: Boolean }) private _headerChecked = false;
|
|
||||||
@property({ type: Boolean }) private _headerIndeterminate = false;
|
|
||||||
@property({ type: Array }) private _checkedRows: string[] = [];
|
|
||||||
@property({ type: String }) private _filter = "";
|
@property({ type: String }) private _filter = "";
|
||||||
@property({ type: String }) private _sortColumn?: string;
|
@property({ type: String }) private _sortColumn?: string;
|
||||||
@property({ type: String }) private _sortDirection: SortingDirection = null;
|
@property({ type: String }) private _sortDirection: SortingDirection = null;
|
||||||
@property({ type: Array }) private _filteredData: DataTableRowData[] = [];
|
@property({ type: Array }) private _filteredData: DataTableRowData[] = [];
|
||||||
|
@query("slot[name='header']") private _header!: HTMLSlotElement;
|
||||||
|
@query(".mdc-data-table__table") private _table!: HTMLDivElement;
|
||||||
|
private _checkableRowsCount?: number;
|
||||||
|
private _checkedRows: string[] = [];
|
||||||
private _sortColumns: {
|
private _sortColumns: {
|
||||||
[key: string]: DataTableSortColumnData;
|
[key: string]: DataTableSortColumnData;
|
||||||
} = {};
|
} = {};
|
||||||
@@ -111,18 +108,17 @@ export class HaDataTable extends BaseElement {
|
|||||||
(value: string) => {
|
(value: string) => {
|
||||||
this._filter = value;
|
this._filter = value;
|
||||||
},
|
},
|
||||||
200,
|
100,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
public clearSelection(): void {
|
public clearSelection(): void {
|
||||||
this._headerChecked = false;
|
this._checkedRows = [];
|
||||||
this._headerIndeterminate = false;
|
this._checkedRowsChanged();
|
||||||
this.mdcFoundation.handleHeaderRowCheckboxChange();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated() {
|
protected firstUpdated(properties: PropertyValues) {
|
||||||
super.firstUpdated();
|
super.firstUpdated(properties);
|
||||||
this._worker = sortFilterWorker();
|
this._worker = sortFilterWorker();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,6 +152,12 @@ export class HaDataTable extends BaseElement {
|
|||||||
this._debounceSearch(this.filter);
|
this._debounceSearch(this.filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (properties.has("data")) {
|
||||||
|
this._checkableRowsCount = this.data.filter(
|
||||||
|
(row) => row.selectable !== false
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
properties.has("data") ||
|
properties.has("data") ||
|
||||||
properties.has("columns") ||
|
properties.has("columns") ||
|
||||||
@@ -170,7 +172,7 @@ export class HaDataTable extends BaseElement {
|
|||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
<div class="mdc-data-table">
|
<div class="mdc-data-table">
|
||||||
<slot name="header">
|
<slot name="header" @slotchange=${this._calcTableHeight}>
|
||||||
${this._filterable
|
${this._filterable
|
||||||
? html`
|
? html`
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
@@ -181,92 +183,119 @@ export class HaDataTable extends BaseElement {
|
|||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
</slot>
|
</slot>
|
||||||
<table class="mdc-data-table__table">
|
<div
|
||||||
<thead>
|
class="mdc-data-table__table ${classMap({
|
||||||
<tr class="mdc-data-table__header-row">
|
"auto-height": this.autoHeight,
|
||||||
${this.selectable
|
})}"
|
||||||
? html`
|
style=${styleMap({
|
||||||
<th
|
height: this.autoHeight
|
||||||
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
|
? `${this._filteredData.length * 53 + 57}px`
|
||||||
role="columnheader"
|
: `calc(100% - ${this._header?.clientHeight}px)`,
|
||||||
scope="col"
|
})}
|
||||||
>
|
>
|
||||||
<ha-checkbox
|
<div class="mdc-data-table__header-row">
|
||||||
class="mdc-data-table__row-checkbox"
|
${this.selectable
|
||||||
@change=${this._handleHeaderRowCheckboxChange}
|
? html`
|
||||||
.indeterminate=${this._headerIndeterminate}
|
<div
|
||||||
.checked=${this._headerChecked}
|
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
|
||||||
>
|
|
||||||
</ha-checkbox>
|
|
||||||
</th>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
${Object.entries(this.columns).map((columnEntry) => {
|
|
||||||
const [key, column] = columnEntry;
|
|
||||||
const sorted = key === this._sortColumn;
|
|
||||||
const classes = {
|
|
||||||
"mdc-data-table__header-cell--numeric": Boolean(
|
|
||||||
column.type && column.type === "numeric"
|
|
||||||
),
|
|
||||||
"mdc-data-table__header-cell--icon": Boolean(
|
|
||||||
column.type && column.type === "icon"
|
|
||||||
),
|
|
||||||
sortable: Boolean(column.sortable),
|
|
||||||
"not-sorted": Boolean(column.sortable && !sorted),
|
|
||||||
};
|
|
||||||
return html`
|
|
||||||
<th
|
|
||||||
class="mdc-data-table__header-cell ${classMap(classes)}"
|
|
||||||
role="columnheader"
|
role="columnheader"
|
||||||
scope="col"
|
scope="col"
|
||||||
@click=${this._handleHeaderClick}
|
|
||||||
data-column-id="${key}"
|
|
||||||
>
|
>
|
||||||
${column.sortable
|
<ha-checkbox
|
||||||
? html`
|
class="mdc-data-table__row-checkbox"
|
||||||
<ha-icon
|
@change=${this._handleHeaderRowCheckboxClick}
|
||||||
.icon=${sorted && this._sortDirection === "desc"
|
.indeterminate=${this._checkedRows.length &&
|
||||||
? "hass:arrow-down"
|
this._checkedRows.length !== this._checkableRowsCount}
|
||||||
: "hass:arrow-up"}
|
.checked=${this._checkedRows.length ===
|
||||||
></ha-icon>
|
this._checkableRowsCount}
|
||||||
`
|
>
|
||||||
: ""}
|
</ha-checkbox>
|
||||||
<span>${column.title}</span>
|
</div>
|
||||||
</th>
|
`
|
||||||
`;
|
: ""}
|
||||||
})}
|
${Object.entries(this.columns).map((columnEntry) => {
|
||||||
</tr>
|
const [key, column] = columnEntry;
|
||||||
</thead>
|
const sorted = key === this._sortColumn;
|
||||||
<tbody class="mdc-data-table__content">
|
const classes = {
|
||||||
${repeat(
|
"mdc-data-table__header-cell--numeric": Boolean(
|
||||||
this._filteredData!,
|
column.type && column.type === "numeric"
|
||||||
(row: DataTableRowData) => row[this.id],
|
),
|
||||||
(row: DataTableRowData) => html`
|
"mdc-data-table__header-cell--icon": Boolean(
|
||||||
<tr
|
column.type && column.type === "icon"
|
||||||
data-row-id="${row[this.id]}"
|
),
|
||||||
|
sortable: Boolean(column.sortable),
|
||||||
|
"not-sorted": Boolean(column.sortable && !sorted),
|
||||||
|
grows: Boolean(column.grows),
|
||||||
|
};
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="mdc-data-table__header-cell ${classMap(classes)}"
|
||||||
|
style=${column.width
|
||||||
|
? styleMap({
|
||||||
|
[column.grows ? "minWidth" : "width"]: String(
|
||||||
|
column.width
|
||||||
|
),
|
||||||
|
})
|
||||||
|
: ""}
|
||||||
|
role="columnheader"
|
||||||
|
scope="col"
|
||||||
|
@click=${this._handleHeaderClick}
|
||||||
|
.columnId=${key}
|
||||||
|
>
|
||||||
|
${column.sortable
|
||||||
|
? html`
|
||||||
|
<ha-icon
|
||||||
|
.icon=${sorted && this._sortDirection === "desc"
|
||||||
|
? "hass:arrow-down"
|
||||||
|
: "hass:arrow-up"}
|
||||||
|
></ha-icon>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<span>${column.title}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div class="mdc-data-table__content scroller">
|
||||||
|
${scroll({
|
||||||
|
items: this._filteredData,
|
||||||
|
renderItem: (row: DataTableRowData) => html`
|
||||||
|
<div
|
||||||
|
.rowId="${row[this.id]}"
|
||||||
@click=${this._handleRowClick}
|
@click=${this._handleRowClick}
|
||||||
class="mdc-data-table__row"
|
class="mdc-data-table__row ${classMap({
|
||||||
|
"mdc-data-table__row--selected": this._checkedRows.includes(
|
||||||
|
String(row[this.id])
|
||||||
|
),
|
||||||
|
})}"
|
||||||
|
aria-selected=${ifDefined(
|
||||||
|
this._checkedRows.includes(String(row[this.id]))
|
||||||
|
? true
|
||||||
|
: undefined
|
||||||
|
)}
|
||||||
|
.selectable=${row.selectable !== false}
|
||||||
>
|
>
|
||||||
${this.selectable
|
${this.selectable
|
||||||
? html`
|
? html`
|
||||||
<td
|
<div
|
||||||
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
|
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
|
||||||
>
|
>
|
||||||
<ha-checkbox
|
<ha-checkbox
|
||||||
class="mdc-data-table__row-checkbox"
|
class="mdc-data-table__row-checkbox"
|
||||||
@change=${this._handleRowCheckboxChange}
|
@change=${this._handleRowCheckboxClick}
|
||||||
|
.disabled=${row.selectable === false}
|
||||||
.checked=${this._checkedRows.includes(
|
.checked=${this._checkedRows.includes(
|
||||||
String(row[this.id])
|
String(row[this.id])
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
</ha-checkbox>
|
</ha-checkbox>
|
||||||
</td>
|
</div>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
${Object.entries(this.columns).map((columnEntry) => {
|
${Object.entries(this.columns).map((columnEntry) => {
|
||||||
const [key, column] = columnEntry;
|
const [key, column] = columnEntry;
|
||||||
return html`
|
return html`
|
||||||
<td
|
<div
|
||||||
class="mdc-data-table__cell ${classMap({
|
class="mdc-data-table__cell ${classMap({
|
||||||
"mdc-data-table__cell--numeric": Boolean(
|
"mdc-data-table__cell--numeric": Boolean(
|
||||||
column.type && column.type === "numeric"
|
column.type && column.type === "numeric"
|
||||||
@@ -274,65 +303,31 @@ export class HaDataTable extends BaseElement {
|
|||||||
"mdc-data-table__cell--icon": Boolean(
|
"mdc-data-table__cell--icon": Boolean(
|
||||||
column.type && column.type === "icon"
|
column.type && column.type === "icon"
|
||||||
),
|
),
|
||||||
|
grows: Boolean(column.grows),
|
||||||
})}"
|
})}"
|
||||||
|
style=${column.width
|
||||||
|
? styleMap({
|
||||||
|
[column.grows ? "minWidth" : "width"]: String(
|
||||||
|
column.width
|
||||||
|
),
|
||||||
|
})
|
||||||
|
: ""}
|
||||||
>
|
>
|
||||||
${column.template
|
${column.template
|
||||||
? column.template(row[key], row)
|
? column.template(row[key], row)
|
||||||
: row[key]}
|
: row[key]}
|
||||||
</td>
|
</div>
|
||||||
`;
|
`;
|
||||||
})}
|
})}
|
||||||
</tr>
|
</div>
|
||||||
`
|
`,
|
||||||
)}
|
})}
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createAdapter(): MDCDataTableAdapter {
|
|
||||||
return {
|
|
||||||
addClassAtRowIndex: (rowIndex: number, cssClasses: string) => {
|
|
||||||
this.rowElements[rowIndex].classList.add(cssClasses);
|
|
||||||
},
|
|
||||||
getRowCount: () => this.data.length,
|
|
||||||
getRowElements: () => this.rowElements,
|
|
||||||
getRowIdAtIndex: (rowIndex: number) => this._getRowIdAtIndex(rowIndex),
|
|
||||||
getRowIndexByChildElement: (el: Element) =>
|
|
||||||
Array.prototype.indexOf.call(this.rowElements, el.closest("tr")),
|
|
||||||
getSelectedRowCount: () => this._checkedRows.length,
|
|
||||||
isCheckboxAtRowIndexChecked: (rowIndex: number) =>
|
|
||||||
this._checkedRows.includes(this._getRowIdAtIndex(rowIndex)),
|
|
||||||
isHeaderRowCheckboxChecked: () => this._headerChecked,
|
|
||||||
isRowsSelectable: () => true,
|
|
||||||
notifyRowSelectionChanged: () => undefined,
|
|
||||||
notifySelectedAll: () => undefined,
|
|
||||||
notifyUnselectedAll: () => undefined,
|
|
||||||
registerHeaderRowCheckbox: () => undefined,
|
|
||||||
registerRowCheckboxes: () => undefined,
|
|
||||||
removeClassAtRowIndex: (rowIndex: number, cssClasses: string) => {
|
|
||||||
this.rowElements[rowIndex].classList.remove(cssClasses);
|
|
||||||
},
|
|
||||||
setAttributeAtRowIndex: (
|
|
||||||
rowIndex: number,
|
|
||||||
attr: string,
|
|
||||||
value: string
|
|
||||||
) => {
|
|
||||||
this.rowElements[rowIndex].setAttribute(attr, value);
|
|
||||||
},
|
|
||||||
setHeaderRowCheckboxChecked: (checked: boolean) => {
|
|
||||||
this._headerChecked = checked;
|
|
||||||
},
|
|
||||||
setHeaderRowCheckboxIndeterminate: (indeterminate: boolean) => {
|
|
||||||
this._headerIndeterminate = indeterminate;
|
|
||||||
},
|
|
||||||
setRowCheckboxCheckedAtIndex: (rowIndex: number, checked: boolean) => {
|
|
||||||
this._setRowChecked(this._getRowIdAtIndex(rowIndex), checked);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _filterData() {
|
private async _filterData() {
|
||||||
const startTime = new Date().getTime();
|
const startTime = new Date().getTime();
|
||||||
this.curRequest++;
|
this.curRequest++;
|
||||||
@@ -360,14 +355,10 @@ export class HaDataTable extends BaseElement {
|
|||||||
this._filteredData = data;
|
this._filteredData = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getRowIdAtIndex(rowIndex: number): string {
|
|
||||||
return this.rowElements[rowIndex].getAttribute("data-row-id")!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleHeaderClick(ev: Event) {
|
private _handleHeaderClick(ev: Event) {
|
||||||
const columnId = (ev.target as HTMLElement)
|
const columnId = ((ev.target as HTMLElement).closest(
|
||||||
.closest("th")!
|
".mdc-data-table__header-cell"
|
||||||
.getAttribute("data-column-id")!;
|
) as any).columnId;
|
||||||
if (!this.columns[columnId].sortable) {
|
if (!this.columns[columnId].sortable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -387,19 +378,32 @@ export class HaDataTable extends BaseElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleHeaderRowCheckboxChange(ev: Event) {
|
private _handleHeaderRowCheckboxClick(ev: Event) {
|
||||||
const checkbox = ev.target as HaCheckbox;
|
const checkbox = ev.target as HaCheckbox;
|
||||||
this._headerChecked = checkbox.checked;
|
if (checkbox.checked) {
|
||||||
this._headerIndeterminate = checkbox.indeterminate;
|
this._checkedRows = this._filteredData
|
||||||
this.mdcFoundation.handleHeaderRowCheckboxChange();
|
.filter((data) => data.selectable !== false)
|
||||||
|
.map((data) => data[this.id]);
|
||||||
|
this._checkedRowsChanged();
|
||||||
|
} else {
|
||||||
|
this._checkedRows = [];
|
||||||
|
this._checkedRowsChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleRowCheckboxChange(ev: Event) {
|
private _handleRowCheckboxClick(ev: Event) {
|
||||||
const checkbox = ev.target as HaCheckbox;
|
const checkbox = ev.target as HaCheckbox;
|
||||||
const rowId = checkbox.closest("tr")!.getAttribute("data-row-id");
|
const rowId = (checkbox.closest(".mdc-data-table__row") as any).rowId;
|
||||||
|
|
||||||
this._setRowChecked(rowId!, checkbox.checked);
|
if (checkbox.checked) {
|
||||||
this.mdcFoundation.handleRowCheckboxChange(ev);
|
if (this._checkedRows.includes(rowId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._checkedRows = [...this._checkedRows, rowId];
|
||||||
|
} else {
|
||||||
|
this._checkedRows = this._checkedRows.filter((row) => row !== rowId);
|
||||||
|
}
|
||||||
|
this._checkedRowsChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleRowClick(ev: Event) {
|
private _handleRowClick(ev: Event) {
|
||||||
@@ -407,26 +411,15 @@ export class HaDataTable extends BaseElement {
|
|||||||
if (target.tagName === "HA-CHECKBOX") {
|
if (target.tagName === "HA-CHECKBOX") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rowId = target.closest("tr")!.getAttribute("data-row-id")!;
|
const rowId = (target.closest(".mdc-data-table__row") as any).rowId;
|
||||||
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
|
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setRowChecked(rowId: string, checked: boolean) {
|
private _checkedRowsChanged() {
|
||||||
if (checked) {
|
// force scroller to update, change it's items
|
||||||
if (this._checkedRows.includes(rowId)) {
|
this._filteredData = [...this._filteredData];
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._checkedRows = [...this._checkedRows, rowId];
|
|
||||||
} else {
|
|
||||||
const index = this._checkedRows.indexOf(rowId);
|
|
||||||
if (index === -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._checkedRows.splice(index, 1);
|
|
||||||
}
|
|
||||||
fireEvent(this, "selection-changed", {
|
fireEvent(this, "selection-changed", {
|
||||||
id: rowId,
|
value: this._checkedRows,
|
||||||
selected: checked,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,10 +427,20 @@ export class HaDataTable extends BaseElement {
|
|||||||
this._debounceSearch(ev.detail.value);
|
this._debounceSearch(ev.detail.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _calcTableHeight() {
|
||||||
|
if (this.autoHeight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.updateComplete;
|
||||||
|
this._table.style.height = `calc(100% - ${this._header.clientHeight}px)`;
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult {
|
static get styles(): CSSResult {
|
||||||
return css`
|
return css`
|
||||||
/* default mdc styles, colors changed, without checkbox styles */
|
/* default mdc styles, colors changed, without checkbox styles */
|
||||||
|
:host {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
.mdc-data-table__content {
|
.mdc-data-table__content {
|
||||||
font-family: Roboto, sans-serif;
|
font-family: Roboto, sans-serif;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
@@ -459,7 +462,7 @@ export class HaDataTable extends BaseElement {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow-x: auto;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdc-data-table__row--selected {
|
.mdc-data-table__row--selected {
|
||||||
@@ -467,12 +470,13 @@ export class HaDataTable extends BaseElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mdc-data-table__row {
|
.mdc-data-table__row {
|
||||||
border-top-color: rgba(var(--rgb-primary-text-color), 0.12);
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 52px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdc-data-table__row {
|
.mdc-data-table__row ~ .mdc-data-table__row {
|
||||||
border-top-width: 1px;
|
border-top: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
||||||
border-top-style: solid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
|
.mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
|
||||||
@@ -489,16 +493,24 @@ export class HaDataTable extends BaseElement {
|
|||||||
|
|
||||||
.mdc-data-table__header-row {
|
.mdc-data-table__header-row {
|
||||||
height: 56px;
|
height: 56px;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdc-data-table__row {
|
.mdc-data-table__header-row::-webkit-scrollbar {
|
||||||
height: 52px;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdc-data-table__cell,
|
.mdc-data-table__cell,
|
||||||
.mdc-data-table__header-cell {
|
.mdc-data-table__header-cell {
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
|
align-self: center;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdc-data-table__header-cell--checkbox,
|
.mdc-data-table__header-cell--checkbox,
|
||||||
@@ -507,6 +519,7 @@ export class HaDataTable extends BaseElement {
|
|||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
/* @noflip */
|
/* @noflip */
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
|
width: 40px;
|
||||||
}
|
}
|
||||||
[dir="rtl"] .mdc-data-table__header-cell--checkbox,
|
[dir="rtl"] .mdc-data-table__header-cell--checkbox,
|
||||||
.mdc-data-table__header-cell--checkbox[dir="rtl"],
|
.mdc-data-table__header-cell--checkbox[dir="rtl"],
|
||||||
@@ -519,10 +532,10 @@ export class HaDataTable extends BaseElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mdc-data-table__table {
|
.mdc-data-table__table {
|
||||||
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdc-data-table__cell {
|
.mdc-data-table__cell {
|
||||||
@@ -551,6 +564,27 @@ export class HaDataTable extends BaseElement {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mdc-data-table__header-cell--icon,
|
||||||
|
.mdc-data-table__cell--icon {
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdc-data-table__header-cell.mdc-data-table__header-cell--icon {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:hover,
|
||||||
|
.mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:not(.not-sorted) {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdc-data-table__cell--icon:first-child ha-icon {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdc-data-table__cell--icon:first-child state-badge {
|
||||||
|
margin-right: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
.mdc-data-table__header-cell {
|
.mdc-data-table__header-cell {
|
||||||
font-family: Roboto, sans-serif;
|
font-family: Roboto, sans-serif;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
@@ -572,48 +606,83 @@ export class HaDataTable extends BaseElement {
|
|||||||
.mdc-data-table__header-cell--numeric {
|
.mdc-data-table__header-cell--numeric {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
.mdc-data-table__header-cell--numeric.sortable:hover,
|
||||||
|
.mdc-data-table__header-cell--numeric.sortable:not(.not-sorted) {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
[dir="rtl"] .mdc-data-table__header-cell--numeric,
|
[dir="rtl"] .mdc-data-table__header-cell--numeric,
|
||||||
.mdc-data-table__header-cell--numeric[dir="rtl"] {
|
.mdc-data-table__header-cell--numeric[dir="rtl"] {
|
||||||
/* @noflip */
|
/* @noflip */
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdc-data-table__header-cell--icon {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* custom from here */
|
/* custom from here */
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.mdc-data-table {
|
.mdc-data-table {
|
||||||
display: block;
|
display: block;
|
||||||
|
border-width: var(--data-table-border-width, 1px);
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
.mdc-data-table__header-cell {
|
.mdc-data-table__header-cell {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
.mdc-data-table__header-cell span {
|
||||||
|
position: relative;
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.mdc-data-table__header-cell.sortable {
|
.mdc-data-table__header-cell.sortable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__header-cell--numeric):not(.mdc-data-table__header-cell--icon)
|
.mdc-data-table__header-cell > * {
|
||||||
span {
|
transition: left 0.2s ease;
|
||||||
position: relative;
|
|
||||||
left: -24px;
|
|
||||||
}
|
}
|
||||||
.mdc-data-table__header-cell.not-sorted > * {
|
.mdc-data-table__header-cell ha-icon {
|
||||||
transition: left 0.2s ease 0s;
|
top: -3px;
|
||||||
|
position: absolute;
|
||||||
}
|
}
|
||||||
.mdc-data-table__header-cell.not-sorted ha-icon {
|
.mdc-data-table__header-cell.not-sorted ha-icon {
|
||||||
left: -36px;
|
left: -20px;
|
||||||
}
|
}
|
||||||
.mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__header-cell--numeric):not(.mdc-data-table__header-cell--icon):hover
|
.mdc-data-table__header-cell.sortable:not(.not-sorted) span,
|
||||||
span {
|
.mdc-data-table__header-cell.sortable.not-sorted:hover span {
|
||||||
left: 0px;
|
left: 24px;
|
||||||
}
|
}
|
||||||
.mdc-data-table__header-cell:hover.not-sorted ha-icon {
|
.mdc-data-table__header-cell.sortable:not(.not-sorted) ha-icon,
|
||||||
left: 0px;
|
.mdc-data-table__header-cell.sortable:hover.not-sorted ha-icon {
|
||||||
|
left: 12px;
|
||||||
}
|
}
|
||||||
.table-header {
|
.table-header {
|
||||||
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
||||||
}
|
}
|
||||||
|
search-input {
|
||||||
|
position: relative;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
slot[name="header"] {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.scroller {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
contain: strict;
|
||||||
|
height: calc(100% - 57px);
|
||||||
|
}
|
||||||
|
.mdc-data-table__table:not(.auto-height) .scroller {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.grows {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { fireEvent } from "../../common/dom/fire_event";
|
|||||||
import {
|
import {
|
||||||
DeviceRegistryEntry,
|
DeviceRegistryEntry,
|
||||||
subscribeDeviceRegistry,
|
subscribeDeviceRegistry,
|
||||||
|
DeviceEntityLookup,
|
||||||
} from "../../data/device_registry";
|
} from "../../data/device_registry";
|
||||||
import { compare } from "../../common/string/compare";
|
import { compare } from "../../common/string/compare";
|
||||||
import { PolymerChangedEvent } from "../../polymer-types";
|
import { PolymerChangedEvent } from "../../polymer-types";
|
||||||
@@ -30,7 +31,6 @@ import {
|
|||||||
AreaRegistryEntry,
|
AreaRegistryEntry,
|
||||||
subscribeAreaRegistry,
|
subscribeAreaRegistry,
|
||||||
} from "../../data/area_registry";
|
} from "../../data/area_registry";
|
||||||
import { DeviceEntityLookup } from "../../panels/config/devices/ha-devices-data-table";
|
|
||||||
import {
|
import {
|
||||||
EntityRegistryEntry,
|
EntityRegistryEntry,
|
||||||
subscribeEntityRegistry,
|
subscribeEntityRegistry,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
DeviceRegistryEntry,
|
DeviceRegistryEntry,
|
||||||
subscribeDeviceRegistry,
|
subscribeDeviceRegistry,
|
||||||
computeDeviceName,
|
computeDeviceName,
|
||||||
|
DeviceEntityLookup,
|
||||||
} from "../../data/device_registry";
|
} from "../../data/device_registry";
|
||||||
import { compare } from "../../common/string/compare";
|
import { compare } from "../../common/string/compare";
|
||||||
import { PolymerChangedEvent } from "../../polymer-types";
|
import { PolymerChangedEvent } from "../../polymer-types";
|
||||||
@@ -29,7 +30,6 @@ import {
|
|||||||
AreaRegistryEntry,
|
AreaRegistryEntry,
|
||||||
subscribeAreaRegistry,
|
subscribeAreaRegistry,
|
||||||
} from "../../data/area_registry";
|
} from "../../data/area_registry";
|
||||||
import { DeviceEntityLookup } from "../../panels/config/devices/ha-devices-data-table";
|
|
||||||
import {
|
import {
|
||||||
EntityRegistryEntry,
|
EntityRegistryEntry,
|
||||||
subscribeEntityRegistry,
|
subscribeEntityRegistry,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
|
|||||||
import { timeOut } from "@polymer/polymer/lib/utils/async";
|
import { timeOut } from "@polymer/polymer/lib/utils/async";
|
||||||
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
|
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
|
||||||
|
|
||||||
import formatTime from "../../common/datetime/format_time";
|
import { formatTime } from "../../common/datetime/format_time";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
/* global Chart moment Color */
|
/* global Chart moment Color */
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import {
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
|
||||||
import hassAttributeUtil from "../util/hass-attributes-util";
|
import hassAttributeUtil from "../util/hass-attributes-util";
|
||||||
|
import { until } from "lit-html/directives/until";
|
||||||
|
|
||||||
|
let jsYamlPromise: Promise<typeof import("js-yaml")>;
|
||||||
|
|
||||||
@customElement("ha-attributes")
|
@customElement("ha-attributes")
|
||||||
class HaAttributes extends LitElement {
|
class HaAttributes extends LitElement {
|
||||||
@@ -32,7 +35,7 @@ class HaAttributes extends LitElement {
|
|||||||
<div class="data-entry">
|
<div class="data-entry">
|
||||||
<div class="key">${attribute.replace(/_/g, " ")}</div>
|
<div class="key">${attribute.replace(/_/g, " ")}</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
${this.formatAttributeValue(attribute)}
|
${this.formatAttribute(attribute)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
@@ -63,6 +66,10 @@ class HaAttributes extends LitElement {
|
|||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
pre {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,18 +82,31 @@ class HaAttributes extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatAttributeValue(attribute: string): string {
|
private formatAttribute(attribute: string): string | TemplateResult {
|
||||||
if (!this.stateObj) {
|
if (!this.stateObj) {
|
||||||
return "-";
|
return "-";
|
||||||
}
|
}
|
||||||
const value = this.stateObj.attributes[attribute];
|
const value = this.stateObj.attributes[attribute];
|
||||||
|
return this.formatAttributeValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatAttributeValue(value: any): string | TemplateResult {
|
||||||
if (value === null) {
|
if (value === null) {
|
||||||
return "-";
|
return "-";
|
||||||
}
|
}
|
||||||
if (Array.isArray(value)) {
|
if (
|
||||||
return value.join(", ");
|
(Array.isArray(value) && value.some((val) => val instanceof Object)) ||
|
||||||
|
(!Array.isArray(value) && value instanceof Object)
|
||||||
|
) {
|
||||||
|
if (!jsYamlPromise) {
|
||||||
|
jsYamlPromise = import(/* webpackChunkName: "js-yaml" */ "js-yaml");
|
||||||
|
}
|
||||||
|
const yaml = jsYamlPromise.then((jsYaml) => jsYaml.safeDump(value));
|
||||||
|
return html`
|
||||||
|
<pre>${until(yaml, "")}</pre>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
return value instanceof Object ? JSON.stringify(value, null, 2) : value;
|
return Array.isArray(value) ? value.join(", ") : value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,374 +0,0 @@
|
|||||||
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
|
||||||
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 "../cards/ha-badges-card";
|
|
||||||
import "../cards/ha-card-chooser";
|
|
||||||
import "./ha-demo-badge";
|
|
||||||
|
|
||||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
|
||||||
import { splitByGroups } from "../common/entity/split_by_groups";
|
|
||||||
import { getGroupEntities } from "../common/entity/get_group_entities";
|
|
||||||
|
|
||||||
// mapping domain to size of the card.
|
|
||||||
const DOMAINS_WITH_CARD = {
|
|
||||||
camera: 4,
|
|
||||||
history_graph: 4,
|
|
||||||
media_player: 3,
|
|
||||||
persistent_notification: 0,
|
|
||||||
plant: 3,
|
|
||||||
weather: 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 4 types:
|
|
||||||
// badges: 0 .. 10
|
|
||||||
// before groups < 0
|
|
||||||
// groups: X
|
|
||||||
// rest: 100
|
|
||||||
|
|
||||||
const PRIORITY = {
|
|
||||||
// before groups < 0
|
|
||||||
configurator: -20,
|
|
||||||
persistent_notification: -15,
|
|
||||||
|
|
||||||
// badges have priority >= 0
|
|
||||||
updater: 0,
|
|
||||||
sun: 1,
|
|
||||||
person: 2,
|
|
||||||
device_tracker: 3,
|
|
||||||
alarm_control_panel: 4,
|
|
||||||
timer: 5,
|
|
||||||
sensor: 6,
|
|
||||||
binary_sensor: 7,
|
|
||||||
mailbox: 8,
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPriority = (domain) => (domain in PRIORITY ? PRIORITY[domain] : 100);
|
|
||||||
|
|
||||||
const sortPriority = (domainA, domainB) => domainA.priority - domainB.priority;
|
|
||||||
|
|
||||||
const entitySortBy = (entityA, entityB) => {
|
|
||||||
const nameA = (
|
|
||||||
entityA.attributes.friendly_name || entityA.entity_id
|
|
||||||
).toLowerCase();
|
|
||||||
const nameB = (
|
|
||||||
entityB.attributes.friendly_name || entityB.entity_id
|
|
||||||
).toLowerCase();
|
|
||||||
|
|
||||||
if (nameA < nameB) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (nameA > nameB) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const iterateDomainSorted = (collection, func) => {
|
|
||||||
Object.keys(collection)
|
|
||||||
.map((key) => collection[key])
|
|
||||||
.sort(sortPriority)
|
|
||||||
.forEach((domain) => {
|
|
||||||
domain.states.sort(entitySortBy);
|
|
||||||
func(domain);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
class HaCards extends PolymerElement {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style include="iron-flex iron-flex-factors"></style>
|
|
||||||
<style>
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
padding: 4px 4px 0;
|
|
||||||
transform: translateZ(0);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badges {
|
|
||||||
font-size: 85%;
|
|
||||||
text-align: center;
|
|
||||||
padding-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column {
|
|
||||||
max-width: 500px;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-card-chooser {
|
|
||||||
display: block;
|
|
||||||
margin: 4px 4px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
:host {
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-card-chooser {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 599px) {
|
|
||||||
.column {
|
|
||||||
max-width: 600px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div id="main">
|
|
||||||
<template is="dom-if" if="[[cards.badges.length]]">
|
|
||||||
<div class="badges">
|
|
||||||
<template is="dom-if" if="[[cards.demo]]">
|
|
||||||
<ha-demo-badge></ha-demo-badge>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<ha-badges-card
|
|
||||||
states="[[cards.badges]]"
|
|
||||||
hass="[[hass]]"
|
|
||||||
></ha-badges-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="horizontal layout center-justified">
|
|
||||||
<template is="dom-repeat" items="[[cards.columns]]" as="column">
|
|
||||||
<div class="column flex-1">
|
|
||||||
<template is="dom-repeat" items="[[column]]" as="card">
|
|
||||||
<ha-card-chooser card-data="[[card]]"></ha-card-chooser>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: Object,
|
|
||||||
|
|
||||||
columns: {
|
|
||||||
type: Number,
|
|
||||||
value: 2,
|
|
||||||
},
|
|
||||||
|
|
||||||
states: Object,
|
|
||||||
|
|
||||||
viewVisible: {
|
|
||||||
type: Boolean,
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
orderedGroupEntities: Array,
|
|
||||||
|
|
||||||
cards: Object,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static get observers() {
|
|
||||||
return ["updateCards(columns, states, viewVisible, orderedGroupEntities)"];
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCards(columns, states, viewVisible, orderedGroupEntities) {
|
|
||||||
if (!viewVisible) {
|
|
||||||
if (this.$.main.parentNode) {
|
|
||||||
this.$.main._parentNode = this.$.main.parentNode;
|
|
||||||
this.$.main.parentNode.removeChild(this.$.main);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.$.main.parentNode && this.$.main._parentNode) {
|
|
||||||
this.$.main._parentNode.appendChild(this.$.main);
|
|
||||||
}
|
|
||||||
this._debouncer = Debouncer.debounce(
|
|
||||||
this._debouncer,
|
|
||||||
timeOut.after(10),
|
|
||||||
() => {
|
|
||||||
// Things might have changed since it got scheduled.
|
|
||||||
if (this.viewVisible) {
|
|
||||||
this.cards = this.computeCards(columns, states, orderedGroupEntities);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
emptyCards() {
|
|
||||||
return {
|
|
||||||
demo: false,
|
|
||||||
badges: [],
|
|
||||||
columns: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
computeCards(columns, states, orderedGroupEntities) {
|
|
||||||
const hass = this.hass;
|
|
||||||
|
|
||||||
const cards = this.emptyCards();
|
|
||||||
|
|
||||||
const entityCount = [];
|
|
||||||
for (let i = 0; i < columns; i++) {
|
|
||||||
cards.columns.push([]);
|
|
||||||
entityCount.push(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find column with < 5 entities, else column with lowest count
|
|
||||||
function getIndex(size) {
|
|
||||||
let minIndex = 0;
|
|
||||||
for (let i = 0; i < entityCount.length; i++) {
|
|
||||||
if (entityCount[i] < 5) {
|
|
||||||
minIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (entityCount[i] < entityCount[minIndex]) {
|
|
||||||
minIndex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entityCount[minIndex] += size;
|
|
||||||
|
|
||||||
return minIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addEntitiesCard(name, entities, groupEntity) {
|
|
||||||
if (entities.length === 0) return;
|
|
||||||
|
|
||||||
const owncard = [];
|
|
||||||
const other = [];
|
|
||||||
|
|
||||||
let size = 0;
|
|
||||||
|
|
||||||
entities.forEach((entity) => {
|
|
||||||
const domain = computeStateDomain(entity);
|
|
||||||
|
|
||||||
if (
|
|
||||||
domain in DOMAINS_WITH_CARD &&
|
|
||||||
!entity.attributes.custom_ui_state_card
|
|
||||||
) {
|
|
||||||
owncard.push(entity);
|
|
||||||
size += DOMAINS_WITH_CARD[domain];
|
|
||||||
} else {
|
|
||||||
other.push(entity);
|
|
||||||
size++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add 1 to the size if we're rendering entities card
|
|
||||||
size += other.length > 0;
|
|
||||||
|
|
||||||
const curIndex = getIndex(size);
|
|
||||||
|
|
||||||
if (other.length > 0) {
|
|
||||||
cards.columns[curIndex].push({
|
|
||||||
hass: hass,
|
|
||||||
cardType: "entities",
|
|
||||||
states: other,
|
|
||||||
groupEntity: groupEntity || false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
owncard.forEach((entity) => {
|
|
||||||
cards.columns[curIndex].push({
|
|
||||||
hass: hass,
|
|
||||||
cardType: computeStateDomain(entity),
|
|
||||||
stateObj: entity,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const splitted = splitByGroups(states);
|
|
||||||
if (orderedGroupEntities) {
|
|
||||||
splitted.groups.sort(
|
|
||||||
(gr1, gr2) =>
|
|
||||||
orderedGroupEntities[gr1.entity_id] -
|
|
||||||
orderedGroupEntities[gr2.entity_id]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
splitted.groups.sort(
|
|
||||||
(gr1, gr2) => gr1.attributes.order - gr2.attributes.order
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const badgesColl = {};
|
|
||||||
const beforeGroupColl = {};
|
|
||||||
const afterGroupedColl = {};
|
|
||||||
|
|
||||||
Object.keys(splitted.ungrouped).forEach((key) => {
|
|
||||||
const state = splitted.ungrouped[key];
|
|
||||||
const domain = computeStateDomain(state);
|
|
||||||
|
|
||||||
if (domain === "a") {
|
|
||||||
cards.demo = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const priority = getPriority(domain);
|
|
||||||
let coll;
|
|
||||||
|
|
||||||
if (priority < 0) {
|
|
||||||
coll = beforeGroupColl;
|
|
||||||
} else if (priority < 10) {
|
|
||||||
coll = badgesColl;
|
|
||||||
} else {
|
|
||||||
coll = afterGroupedColl;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(domain in coll)) {
|
|
||||||
coll[domain] = {
|
|
||||||
domain: domain,
|
|
||||||
priority: priority,
|
|
||||||
states: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
coll[domain].states.push(state);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (orderedGroupEntities) {
|
|
||||||
Object.keys(badgesColl)
|
|
||||||
.map((key) => badgesColl[key])
|
|
||||||
.forEach((domain) => {
|
|
||||||
cards.badges.push.apply(cards.badges, domain.states);
|
|
||||||
});
|
|
||||||
|
|
||||||
cards.badges.sort(
|
|
||||||
(e1, e2) =>
|
|
||||||
orderedGroupEntities[e1.entity_id] -
|
|
||||||
orderedGroupEntities[e2.entity_id]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
iterateDomainSorted(badgesColl, (domain) => {
|
|
||||||
cards.badges.push.apply(cards.badges, domain.states);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
iterateDomainSorted(beforeGroupColl, (domain) => {
|
|
||||||
addEntitiesCard(domain.domain, domain.states);
|
|
||||||
});
|
|
||||||
|
|
||||||
splitted.groups.forEach((groupState) => {
|
|
||||||
const entities = getGroupEntities(states, groupState);
|
|
||||||
addEntitiesCard(
|
|
||||||
groupState.entity_id,
|
|
||||||
Object.keys(entities).map((key) => entities[key]),
|
|
||||||
groupState
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
iterateDomainSorted(afterGroupedColl, (domain) => {
|
|
||||||
addEntitiesCard(domain.domain, domain.states);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove empty columns
|
|
||||||
cards.columns = cards.columns.filter((val) => val.length > 0);
|
|
||||||
|
|
||||||
return cards;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define("ha-cards", HaCards);
|
|
||||||
@@ -9,7 +9,7 @@ const MwcCheckbox = customElements.get("mwc-checkbox") as Constructor<Checkbox>;
|
|||||||
|
|
||||||
@customElement("ha-checkbox")
|
@customElement("ha-checkbox")
|
||||||
export class HaCheckbox extends MwcCheckbox {
|
export class HaCheckbox extends MwcCheckbox {
|
||||||
protected firstUpdated() {
|
public firstUpdated() {
|
||||||
super.firstUpdated();
|
super.firstUpdated();
|
||||||
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
|
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
customElement,
|
customElement,
|
||||||
unsafeCSS,
|
unsafeCSS,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
|
import { ripple } from "@material/mwc-ripple/ripple-directive";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
|
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
@@ -33,22 +33,27 @@ export class HaChips extends LitElement {
|
|||||||
${this.items.map(
|
${this.items.map(
|
||||||
(item, idx) =>
|
(item, idx) =>
|
||||||
html`
|
html`
|
||||||
<button
|
<div class="mdc-chip" .index=${idx} @click=${this._handleClick}>
|
||||||
class="mdc-chip"
|
<div class="mdc-chip__ripple" .ripple="${ripple()}"></div>
|
||||||
.index=${idx}
|
<span role="gridcell">
|
||||||
@click=${this._handleClick}
|
<span
|
||||||
>
|
role="button"
|
||||||
<span class="mdc-chip__text">${item}</span>
|
tabindex="0"
|
||||||
</button>
|
class="mdc-chip__primary-action"
|
||||||
|
>
|
||||||
|
<span class="mdc-chip__text">${item}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleClick(ev) {
|
private _handleClick(ev): void {
|
||||||
fireEvent(this, "chip-clicked", {
|
fireEvent(this, "chip-clicked", {
|
||||||
index: ev.target.closest("button").index,
|
index: ev.currentTarget.index,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
import { customElement, CSSResult, css } from "lit-element";
|
import { customElement, CSSResult, css, html } from "lit-element";
|
||||||
|
import "@polymer/paper-icon-button/paper-icon-button";
|
||||||
import "@material/mwc-dialog";
|
import "@material/mwc-dialog";
|
||||||
import { style } from "@material/mwc-dialog/mwc-dialog-css";
|
import { style } from "@material/mwc-dialog/mwc-dialog-css";
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
import { Dialog } from "@material/mwc-dialog";
|
import { Dialog } from "@material/mwc-dialog";
|
||||||
import { Constructor } from "../types";
|
import { Constructor, HomeAssistant } from "../types";
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>;
|
const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>;
|
||||||
|
|
||||||
|
export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
|
||||||
|
${title}
|
||||||
|
<paper-icon-button
|
||||||
|
aria-label=${hass.localize("ui.dialogs.generic.close")}
|
||||||
|
icon="hass:close"
|
||||||
|
dialogAction="close"
|
||||||
|
class="close_button"
|
||||||
|
></paper-icon-button>
|
||||||
|
`;
|
||||||
|
|
||||||
@customElement("ha-dialog")
|
@customElement("ha-dialog")
|
||||||
export class HaDialog extends MwcDialog {
|
export class HaDialog extends MwcDialog {
|
||||||
protected static get styles(): CSSResult[] {
|
protected static get styles(): CSSResult[] {
|
||||||
@@ -19,6 +30,15 @@ export class HaDialog extends MwcDialog {
|
|||||||
.mdc-dialog__container {
|
.mdc-dialog__container {
|
||||||
align-items: var(--vertial-align-dialog, center);
|
align-items: var(--vertial-align-dialog, center);
|
||||||
}
|
}
|
||||||
|
.mdc-dialog__title::before {
|
||||||
|
display: block;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
.close_button {
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
top: 12px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { classMap } from "lit-html/directives/class-map";
|
import { classMap } from "lit-html/directives/class-map";
|
||||||
import { html, customElement } from "lit-element";
|
import { html, customElement } from "lit-element";
|
||||||
import { ripple } from "@material/mwc-ripple/ripple-directive.js";
|
import { ripple } from "@material/mwc-ripple/ripple-directive";
|
||||||
|
|
||||||
import "@material/mwc-fab";
|
import "@material/mwc-fab";
|
||||||
import { Constructor } from "../types";
|
import { Constructor } from "../types";
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
property,
|
property,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
query,
|
query,
|
||||||
|
CSSResult,
|
||||||
|
css,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import {
|
import {
|
||||||
HaFormElement,
|
HaFormElement,
|
||||||
@@ -19,13 +21,14 @@ import "@polymer/paper-input/paper-input";
|
|||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||||
import { PaperSliderElement } from "@polymer/paper-slider/paper-slider";
|
import { PaperSliderElement } from "@polymer/paper-slider/paper-slider";
|
||||||
|
import { HaCheckbox } from "../ha-checkbox";
|
||||||
|
|
||||||
@customElement("ha-form-integer")
|
@customElement("ha-form-integer")
|
||||||
export class HaFormInteger extends LitElement implements HaFormElement {
|
export class HaFormInteger extends LitElement implements HaFormElement {
|
||||||
@property() public schema!: HaFormIntegerSchema;
|
@property() public schema!: HaFormIntegerSchema;
|
||||||
@property() public data!: HaFormIntegerData;
|
@property() public data?: HaFormIntegerData;
|
||||||
@property() public label!: string;
|
@property() public label?: string;
|
||||||
@property() public suffix!: string;
|
@property() public suffix?: string;
|
||||||
@query("paper-input ha-paper-slider") private _input?: HTMLElement;
|
@query("paper-input ha-paper-slider") private _input?: HTMLElement;
|
||||||
|
|
||||||
public focus() {
|
public focus() {
|
||||||
@@ -39,20 +42,31 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
|||||||
? html`
|
? html`
|
||||||
<div>
|
<div>
|
||||||
${this.label}
|
${this.label}
|
||||||
<ha-paper-slider
|
<div class="flex">
|
||||||
pin=""
|
${this.schema.optional && this.schema.default === undefined
|
||||||
.value=${this._value}
|
? html`
|
||||||
.min=${this.schema.valueMin}
|
<ha-checkbox
|
||||||
.max=${this.schema.valueMax}
|
@change=${this._handleCheckboxChange}
|
||||||
@value-changed=${this._valueChanged}
|
.checked=${this.data !== undefined}
|
||||||
></ha-paper-slider>
|
></ha-checkbox>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<ha-paper-slider
|
||||||
|
pin=""
|
||||||
|
.value=${this._value}
|
||||||
|
.min=${this.schema.valueMin}
|
||||||
|
.max=${this.schema.valueMax}
|
||||||
|
.disabled=${this.data === undefined}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
></ha-paper-slider>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<paper-input
|
<paper-input
|
||||||
type="number"
|
type="number"
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.value=${this.data}
|
.value=${this._value}
|
||||||
.required=${this.schema.required}
|
.required=${this.schema.required}
|
||||||
.autoValidate=${this.schema.required}
|
.autoValidate=${this.schema.required}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
@@ -61,7 +75,14 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private get _value() {
|
private get _value() {
|
||||||
return this.data || 0;
|
return this.data || this.schema.default || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleCheckboxChange(ev: Event) {
|
||||||
|
const checked = (ev.target as HaCheckbox).checked;
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: checked ? this._value : undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _valueChanged(ev: Event) {
|
private _valueChanged(ev: Event) {
|
||||||
@@ -75,6 +96,14 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
|||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
158
src/components/ha-form/ha-form-multi_select.ts
Normal file
158
src/components/ha-form/ha-form-multi_select.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import "@polymer/paper-checkbox/paper-checkbox";
|
||||||
|
import "@polymer/paper-menu-button/paper-menu-button";
|
||||||
|
import "@polymer/paper-input/paper-input";
|
||||||
|
import "@polymer/paper-item/paper-icon-item";
|
||||||
|
import "@polymer/paper-listbox/paper-listbox";
|
||||||
|
import "@polymer/paper-ripple/paper-ripple";
|
||||||
|
import {
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
query,
|
||||||
|
TemplateResult,
|
||||||
|
CSSResult,
|
||||||
|
css,
|
||||||
|
} from "lit-element";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import {
|
||||||
|
HaFormElement,
|
||||||
|
HaFormMultiSelectData,
|
||||||
|
HaFormMultiSelectSchema,
|
||||||
|
} from "./ha-form";
|
||||||
|
|
||||||
|
@customElement("ha-form-multi_select")
|
||||||
|
export class HaFormMultiSelect extends LitElement implements HaFormElement {
|
||||||
|
@property() public schema!: HaFormMultiSelectSchema;
|
||||||
|
@property() public data!: HaFormMultiSelectData;
|
||||||
|
@property() public label!: string;
|
||||||
|
@property() public suffix!: string;
|
||||||
|
@property() private _init = false;
|
||||||
|
@query("paper-menu-button") private _input?: HTMLElement;
|
||||||
|
|
||||||
|
public focus(): void {
|
||||||
|
if (this._input) {
|
||||||
|
this._input.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
const options = Array.isArray(this.schema.options)
|
||||||
|
? this.schema.options
|
||||||
|
: Object.entries(this.schema.options!);
|
||||||
|
|
||||||
|
const data = this.data || [];
|
||||||
|
return html`
|
||||||
|
<paper-menu-button horizontal-align="right" vertical-offset="8">
|
||||||
|
<div class="dropdown-trigger" slot="dropdown-trigger">
|
||||||
|
<paper-ripple></paper-ripple>
|
||||||
|
<paper-input
|
||||||
|
id="input"
|
||||||
|
type="text"
|
||||||
|
readonly
|
||||||
|
value=${data
|
||||||
|
.map((value) => this.schema.options![value] || value)
|
||||||
|
.join(", ")}
|
||||||
|
label=${this.label}
|
||||||
|
input-role="button"
|
||||||
|
input-aria-haspopup="listbox"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<iron-icon
|
||||||
|
icon="paper-dropdown-menu:arrow-drop-down"
|
||||||
|
suffix
|
||||||
|
slot="suffix"
|
||||||
|
></iron-icon>
|
||||||
|
</paper-input>
|
||||||
|
</div>
|
||||||
|
<paper-listbox
|
||||||
|
multi
|
||||||
|
slot="dropdown-content"
|
||||||
|
attr-for-selected="item-value"
|
||||||
|
.selectedValues=${data}
|
||||||
|
@selected-items-changed=${this._valueChanged}
|
||||||
|
@iron-select=${this._onSelect}
|
||||||
|
>
|
||||||
|
${// TS doesn't work with union array types https://github.com/microsoft/TypeScript/issues/36390
|
||||||
|
// @ts-ignore
|
||||||
|
options.map((item: string | [string, string]) => {
|
||||||
|
const value = this._optionValue(item);
|
||||||
|
return html`
|
||||||
|
<paper-icon-item .itemValue=${value}>
|
||||||
|
<paper-checkbox
|
||||||
|
.checked=${data.includes(value)}
|
||||||
|
slot="item-icon"
|
||||||
|
></paper-checkbox>
|
||||||
|
${this._optionLabel(item)}
|
||||||
|
</paper-icon-item>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</paper-listbox>
|
||||||
|
</paper-menu-button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated() {
|
||||||
|
this.updateComplete.then(() => {
|
||||||
|
const input = (this.shadowRoot?.querySelector("paper-input")
|
||||||
|
?.inputElement as any)?.inputElement;
|
||||||
|
if (input) {
|
||||||
|
input.style.textOverflow = "ellipsis";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _optionValue(item: string | string[]): string {
|
||||||
|
return Array.isArray(item) ? item[0] : item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _optionLabel(item: string | string[]): string {
|
||||||
|
return Array.isArray(item) ? item[1] || item[0] : item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onSelect(ev: Event) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(ev: CustomEvent): void {
|
||||||
|
if (!ev.detail.value || !this._init) {
|
||||||
|
// ignore first call because that is the init of the component
|
||||||
|
this._init = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(
|
||||||
|
this,
|
||||||
|
"value-changed",
|
||||||
|
{
|
||||||
|
value: ev.detail.value.map((element) => element.itemValue),
|
||||||
|
},
|
||||||
|
{ bubbles: false }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
paper-menu-button {
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
--paper-item-icon-width: 34px;
|
||||||
|
}
|
||||||
|
paper-ripple {
|
||||||
|
top: 12px;
|
||||||
|
left: 0px;
|
||||||
|
bottom: 8px;
|
||||||
|
right: 0px;
|
||||||
|
}
|
||||||
|
paper-input {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-form-multi_select": HaFormMultiSelect;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
property,
|
property,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
query,
|
query,
|
||||||
|
CSSResult,
|
||||||
|
css,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form";
|
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
@@ -36,8 +38,10 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
|||||||
.selected=${this.data}
|
.selected=${this.data}
|
||||||
@selected-item-changed=${this._valueChanged}
|
@selected-item-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
${this.schema.options!.map(
|
${// TS doesn't work with union array types https://github.com/microsoft/TypeScript/issues/36390
|
||||||
(item) => html`
|
// @ts-ignore
|
||||||
|
this.schema.options!.map(
|
||||||
|
(item: string | [string, string]) => html`
|
||||||
<paper-item .itemValue=${this._optionValue(item)}>
|
<paper-item .itemValue=${this._optionValue(item)}>
|
||||||
${this._optionLabel(item)}
|
${this._optionLabel(item)}
|
||||||
</paper-item>
|
</paper-item>
|
||||||
@@ -48,12 +52,12 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _optionValue(item) {
|
private _optionValue(item: string | [string, string]) {
|
||||||
return Array.isArray(item) ? item[0] : item;
|
return Array.isArray(item) ? item[0] : item;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _optionLabel(item) {
|
private _optionLabel(item: string | [string, string]) {
|
||||||
return Array.isArray(item) ? item[1] : item;
|
return Array.isArray(item) ? item[1] || item[0] : item;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _valueChanged(ev: CustomEvent) {
|
private _valueChanged(ev: CustomEvent) {
|
||||||
@@ -64,6 +68,14 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
|||||||
value: ev.detail.value.itemValue,
|
value: ev.detail.value.itemValue,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
paper-dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import "./ha-form-integer";
|
|||||||
import "./ha-form-float";
|
import "./ha-form-float";
|
||||||
import "./ha-form-boolean";
|
import "./ha-form-boolean";
|
||||||
import "./ha-form-select";
|
import "./ha-form-select";
|
||||||
|
import "./ha-form-multi_select";
|
||||||
import "./ha-form-positive_time_period_dict";
|
import "./ha-form-positive_time_period_dict";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||||
@@ -22,6 +23,7 @@ export type HaFormSchema =
|
|||||||
| HaFormFloatSchema
|
| HaFormFloatSchema
|
||||||
| HaFormBooleanSchema
|
| HaFormBooleanSchema
|
||||||
| HaFormSelectSchema
|
| HaFormSelectSchema
|
||||||
|
| HaFormMultiSelectSchema
|
||||||
| HaFormTimeSchema;
|
| HaFormTimeSchema;
|
||||||
|
|
||||||
export interface HaFormBaseSchema {
|
export interface HaFormBaseSchema {
|
||||||
@@ -41,7 +43,12 @@ export interface HaFormIntegerSchema extends HaFormBaseSchema {
|
|||||||
|
|
||||||
export interface HaFormSelectSchema extends HaFormBaseSchema {
|
export interface HaFormSelectSchema extends HaFormBaseSchema {
|
||||||
type: "select";
|
type: "select";
|
||||||
options?: string[];
|
options?: string[] | Array<[string, string]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HaFormMultiSelectSchema extends HaFormBaseSchema {
|
||||||
|
type: "multi_select";
|
||||||
|
options?: { [key: string]: string } | string[] | Array<[string, string]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HaFormFloatSchema extends HaFormBaseSchema {
|
export interface HaFormFloatSchema extends HaFormBaseSchema {
|
||||||
@@ -71,6 +78,7 @@ export type HaFormData =
|
|||||||
| HaFormFloatData
|
| HaFormFloatData
|
||||||
| HaFormBooleanData
|
| HaFormBooleanData
|
||||||
| HaFormSelectData
|
| HaFormSelectData
|
||||||
|
| HaFormMultiSelectData
|
||||||
| HaFormTimeData;
|
| HaFormTimeData;
|
||||||
|
|
||||||
export type HaFormStringData = string;
|
export type HaFormStringData = string;
|
||||||
@@ -78,6 +86,7 @@ export type HaFormIntegerData = number;
|
|||||||
export type HaFormFloatData = number;
|
export type HaFormFloatData = number;
|
||||||
export type HaFormBooleanData = boolean;
|
export type HaFormBooleanData = boolean;
|
||||||
export type HaFormSelectData = string;
|
export type HaFormSelectData = string;
|
||||||
|
export type HaFormMultiSelectData = string[];
|
||||||
export interface HaFormTimeData {
|
export interface HaFormTimeData {
|
||||||
hours?: number;
|
hours?: number;
|
||||||
minutes?: number;
|
minutes?: number;
|
||||||
@@ -86,7 +95,7 @@ export interface HaFormTimeData {
|
|||||||
|
|
||||||
export interface HaFormElement extends LitElement {
|
export interface HaFormElement extends LitElement {
|
||||||
schema: HaFormSchema;
|
schema: HaFormSchema;
|
||||||
data: HaFormDataContainer | HaFormData;
|
data?: HaFormDataContainer | HaFormData;
|
||||||
label?: string;
|
label?: string;
|
||||||
suffix?: string;
|
suffix?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
65
src/components/ha-icon-input.ts
Normal file
65
src/components/ha-icon-input.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
LitElement,
|
||||||
|
TemplateResult,
|
||||||
|
property,
|
||||||
|
customElement,
|
||||||
|
} from "lit-element";
|
||||||
|
|
||||||
|
import "@polymer/paper-input/paper-input";
|
||||||
|
import "./ha-icon";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
|
||||||
|
@customElement("ha-icon-input")
|
||||||
|
export class HaIconInput extends LitElement {
|
||||||
|
@property() public value?: string;
|
||||||
|
@property() public label?: string;
|
||||||
|
@property() public placeholder?: string;
|
||||||
|
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<paper-input
|
||||||
|
.value=${this.value}
|
||||||
|
.label=${this.label}
|
||||||
|
.placeholder=${this.placeholder}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
auto-validate
|
||||||
|
.errorMessage=${this.errorMessage}
|
||||||
|
pattern="^\\S+:\\S+$"
|
||||||
|
>
|
||||||
|
${this.value || this.placeholder
|
||||||
|
? html`
|
||||||
|
<ha-icon .icon=${this.value || this.placeholder} slot="suffix">
|
||||||
|
</ha-icon>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</paper-input>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(ev: CustomEvent) {
|
||||||
|
this.value = ev.detail.value;
|
||||||
|
fireEvent(
|
||||||
|
this,
|
||||||
|
"value-changed",
|
||||||
|
{ value: ev.detail.value },
|
||||||
|
{
|
||||||
|
bubbles: false,
|
||||||
|
composed: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
ha-icon {
|
||||||
|
position: relative;
|
||||||
|
bottom: 4px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,6 +53,7 @@ class HaMarkdown extends UpdatingElement {
|
|||||||
node.host !== document.location.host
|
node.host !== document.location.host
|
||||||
) {
|
) {
|
||||||
node.target = "_blank";
|
node.target = "_blank";
|
||||||
|
node.rel = "noreferrer";
|
||||||
|
|
||||||
// protect referrer on external links and deny window.opener access for security reasons
|
// protect referrer on external links and deny window.opener access for security reasons
|
||||||
// (see https://mathiasbynens.github.io/rel-noopener/)
|
// (see https://mathiasbynens.github.io/rel-noopener/)
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ class HaPaperSlider extends PaperSliderClass {
|
|||||||
line-height: normal;
|
line-height: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disabled.ring > .slider-knob > .slider-knob-inner {
|
||||||
|
background-color: var(--paper-slider-disabled-knob-color, var(--paper-grey-400));
|
||||||
|
border: 2px solid var(--paper-slider-disabled-knob-color, var(--paper-grey-400));
|
||||||
|
}
|
||||||
|
|
||||||
.pin > .slider-knob > .slider-knob-inner::before {
|
.pin > .slider-knob > .slider-knob-inner::before {
|
||||||
top: unset;
|
top: unset;
|
||||||
margin-left: unset;
|
margin-left: unset;
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
|
|||||||
if (!this._related) {
|
if (!this._related) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
if (Object.keys(this._related).length === 0) {
|
||||||
|
return html`
|
||||||
|
${this.hass.localize("ui.components.related-items.no_related_found")}
|
||||||
|
`;
|
||||||
|
}
|
||||||
return html`
|
return html`
|
||||||
${this._related.config_entry && this._entries
|
${this._related.config_entry && this._entries
|
||||||
? this._related.config_entry.map((relatedConfigEntryId) => {
|
? this._related.config_entry.map((relatedConfigEntryId) => {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { classMap } from "lit-html/directives/class-map";
|
|||||||
// tslint:disable-next-line: no-duplicate-imports
|
// tslint:disable-next-line: no-duplicate-imports
|
||||||
import { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item";
|
import { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item";
|
||||||
import { computeRTL } from "../common/util/compute_rtl";
|
import { computeRTL } from "../common/util/compute_rtl";
|
||||||
|
import { compare } from "../common/string/compare";
|
||||||
|
|
||||||
const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"];
|
const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"];
|
||||||
|
|
||||||
@@ -46,7 +47,21 @@ const SORT_VALUE_URL_PATHS = {
|
|||||||
config: 11,
|
config: 11,
|
||||||
};
|
};
|
||||||
|
|
||||||
const panelSorter = (a, b) => {
|
const panelSorter = (a: PanelInfo, b: PanelInfo) => {
|
||||||
|
// Put all the Lovelace at the top.
|
||||||
|
const aLovelace = a.component_name === "lovelace";
|
||||||
|
const bLovelace = b.component_name === "lovelace";
|
||||||
|
|
||||||
|
if (aLovelace && bLovelace) {
|
||||||
|
return compare(a.title!, b.title!);
|
||||||
|
}
|
||||||
|
if (aLovelace && !bLovelace) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (bLovelace) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
const aBuiltIn = a.url_path in SORT_VALUE_URL_PATHS;
|
const aBuiltIn = a.url_path in SORT_VALUE_URL_PATHS;
|
||||||
const bBuiltIn = b.url_path in SORT_VALUE_URL_PATHS;
|
const bBuiltIn = b.url_path in SORT_VALUE_URL_PATHS;
|
||||||
|
|
||||||
@@ -60,14 +75,9 @@ const panelSorter = (a, b) => {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
// both not built in, sort by title
|
// both not built in, sort by title
|
||||||
if (a.title! < b.title!) {
|
return compare(a.title!, b.title!);
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (a.title! > b.title!) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
};
|
};
|
||||||
|
const DEFAULT_PAGE = localStorage.defaultPage || DEFAULT_PANEL;
|
||||||
|
|
||||||
const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => {
|
const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => {
|
||||||
const panels = hass.panels;
|
const panels = hass.panels;
|
||||||
@@ -79,7 +89,7 @@ const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => {
|
|||||||
const afterSpacer: PanelInfo[] = [];
|
const afterSpacer: PanelInfo[] = [];
|
||||||
|
|
||||||
Object.values(panels).forEach((panel) => {
|
Object.values(panels).forEach((panel) => {
|
||||||
if (!panel.title) {
|
if (!panel.title || panel.url_path === DEFAULT_PAGE) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
(SHOW_AFTER_SPACER.includes(panel.url_path)
|
(SHOW_AFTER_SPACER.includes(panel.url_path)
|
||||||
@@ -103,8 +113,7 @@ class HaSidebar extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public alwaysExpand = false;
|
@property({ type: Boolean }) public alwaysExpand = false;
|
||||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||||
@property() public _defaultPage?: string =
|
|
||||||
localStorage.defaultPage || DEFAULT_PANEL;
|
|
||||||
@property() private _externalConfig?: ExternalConfig;
|
@property() private _externalConfig?: ExternalConfig;
|
||||||
@property() private _notifications?: PersistentNotification[];
|
@property() private _notifications?: PersistentNotification[];
|
||||||
// property used only in css
|
// property used only in css
|
||||||
@@ -133,6 +142,9 @@ class HaSidebar extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultPanel =
|
||||||
|
this.hass.panels[DEFAULT_PAGE] || this.hass.panels[DEFAULT_PANEL];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
${!this.narrow
|
${!this.narrow
|
||||||
@@ -157,9 +169,9 @@ class HaSidebar extends LitElement {
|
|||||||
@keydown=${this._listboxKeydown}
|
@keydown=${this._listboxKeydown}
|
||||||
>
|
>
|
||||||
${this._renderPanel(
|
${this._renderPanel(
|
||||||
this._defaultPage,
|
defaultPanel.url_path,
|
||||||
"hass:apps",
|
defaultPanel.icon || "hass:view-dashboard",
|
||||||
hass.localize("panel.states")
|
defaultPanel.title || hass.localize("panel.states")
|
||||||
)}
|
)}
|
||||||
${beforeSpacer.map((panel) =>
|
${beforeSpacer.map((panel) =>
|
||||||
this._renderPanel(
|
this._renderPanel(
|
||||||
@@ -597,6 +609,7 @@ class HaSidebar extends LitElement {
|
|||||||
|
|
||||||
paper-icon-item .item-text {
|
paper-icon-item .item-text {
|
||||||
display: none;
|
display: none;
|
||||||
|
max-width: calc(100% - 56px);
|
||||||
}
|
}
|
||||||
:host([expanded]) paper-icon-item .item-text {
|
:host([expanded]) paper-icon-item .item-text {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ import { afterNextRender } from "../common/util/render-status";
|
|||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
import { HaCodeEditor } from "./ha-code-editor";
|
import { HaCodeEditor } from "./ha-code-editor";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// for fire event
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"editor-refreshed": undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isEmpty = (obj: object) => {
|
const isEmpty = (obj: object) => {
|
||||||
if (typeof obj !== "object") {
|
if (typeof obj !== "object") {
|
||||||
return false;
|
return false;
|
||||||
@@ -37,6 +44,7 @@ export class HaYamlEditor extends LitElement {
|
|||||||
if (this._editor?.codemirror) {
|
if (this._editor?.codemirror) {
|
||||||
this._editor.codemirror.refresh();
|
this._editor.codemirror.refresh();
|
||||||
}
|
}
|
||||||
|
afterNextRender(() => fireEvent(this, "editor-refreshed"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ export interface MarkerLocation {
|
|||||||
id: string;
|
id: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
radius_color?: string;
|
radius_color?: string;
|
||||||
editable?: boolean;
|
location_editable?: boolean;
|
||||||
|
radius_editable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement("ha-locations-editor")
|
@customElement("ha-locations-editor")
|
||||||
@@ -208,7 +209,7 @@ export class HaLocationsEditor extends LitElement {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
circle.addTo(this._leafletMap!);
|
circle.addTo(this._leafletMap!);
|
||||||
if (location.editable) {
|
if (location.radius_editable || location.location_editable) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
circle.editing.enable();
|
circle.editing.enable();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -230,19 +231,25 @@ export class HaLocationsEditor extends LitElement {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
(ev: MouseEvent) => this._markerClicked(ev)
|
(ev: MouseEvent) => this._markerClicked(ev)
|
||||||
);
|
);
|
||||||
resizeMarker.addEventListener(
|
if (location.radius_editable) {
|
||||||
"dragend",
|
resizeMarker.addEventListener(
|
||||||
// @ts-ignore
|
"dragend",
|
||||||
(ev: DragEndEvent) => this._updateRadius(ev)
|
// @ts-ignore
|
||||||
);
|
(ev: DragEndEvent) => this._updateRadius(ev)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
resizeMarker.remove();
|
||||||
|
}
|
||||||
this._locationMarkers![location.id] = circle;
|
this._locationMarkers![location.id] = circle;
|
||||||
} else {
|
} else {
|
||||||
this._circles[location.id] = circle;
|
this._circles[location.id] = circle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!location.radius || !location.editable) {
|
if (
|
||||||
|
!location.radius ||
|
||||||
|
(!location.radius_editable && !location.location_editable)
|
||||||
|
) {
|
||||||
const options: MarkerOptions = {
|
const options: MarkerOptions = {
|
||||||
draggable: Boolean(location.editable),
|
|
||||||
title: location.name,
|
title: location.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
311
src/components/map/ha-map.ts
Normal file
311
src/components/map/ha-map.ts
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
import "@polymer/paper-icon-button/paper-icon-button";
|
||||||
|
import { Circle, Layer, Map, Marker } from "leaflet";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit-element";
|
||||||
|
import {
|
||||||
|
LeafletModuleType,
|
||||||
|
setupLeafletMap,
|
||||||
|
} from "../../common/dom/setup-leaflet-map";
|
||||||
|
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||||
|
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||||
|
import { debounce } from "../../common/util/debounce";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
import "../../panels/map/ha-entity-marker";
|
||||||
|
|
||||||
|
@customElement("ha-map")
|
||||||
|
class HaMap extends LitElement {
|
||||||
|
@property() public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public entities?: string[];
|
||||||
|
@property() public darkMode = false;
|
||||||
|
@property() public zoom?: number;
|
||||||
|
// tslint:disable-next-line
|
||||||
|
private Leaflet?: LeafletModuleType;
|
||||||
|
private _leafletMap?: Map;
|
||||||
|
// @ts-ignore
|
||||||
|
private _resizeObserver?: ResizeObserver;
|
||||||
|
private _debouncedResizeListener = debounce(
|
||||||
|
() => {
|
||||||
|
if (!this._leafletMap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._leafletMap.invalidateSize();
|
||||||
|
},
|
||||||
|
100,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
private _mapItems: Array<Marker | Circle> = [];
|
||||||
|
private _mapZones: Array<Marker | Circle> = [];
|
||||||
|
private _connected = false;
|
||||||
|
|
||||||
|
public connectedCallback(): void {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._connected = true;
|
||||||
|
if (this.hasUpdated) {
|
||||||
|
this.loadMap();
|
||||||
|
this._attachObserver();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnectedCallback(): void {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._connected = false;
|
||||||
|
|
||||||
|
if (this._leafletMap) {
|
||||||
|
this._leafletMap.remove();
|
||||||
|
this._leafletMap = undefined;
|
||||||
|
this.Leaflet = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._resizeObserver) {
|
||||||
|
this._resizeObserver.unobserve(this._mapEl);
|
||||||
|
} else {
|
||||||
|
window.removeEventListener("resize", this._debouncedResizeListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this.entities) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<div id="map"></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps: PropertyValues): void {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
this.loadMap();
|
||||||
|
|
||||||
|
if (this._connected) {
|
||||||
|
this._attachObserver();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues): void {
|
||||||
|
if (changedProps.has("hass")) {
|
||||||
|
this._drawEntities();
|
||||||
|
this._fitMap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _mapEl(): HTMLDivElement {
|
||||||
|
return this.shadowRoot!.getElementById("map") as HTMLDivElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadMap(): Promise<void> {
|
||||||
|
[this._leafletMap, this.Leaflet] = await setupLeafletMap(
|
||||||
|
this._mapEl,
|
||||||
|
this.darkMode
|
||||||
|
);
|
||||||
|
this._drawEntities();
|
||||||
|
this._leafletMap.invalidateSize();
|
||||||
|
this._fitMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _fitMap(): void {
|
||||||
|
if (!this._leafletMap || !this.Leaflet || !this.hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._mapItems.length === 0) {
|
||||||
|
this._leafletMap.setView(
|
||||||
|
new this.Leaflet.LatLng(
|
||||||
|
this.hass.config.latitude,
|
||||||
|
this.hass.config.longitude
|
||||||
|
),
|
||||||
|
this.zoom || 14
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bounds = this.Leaflet.latLngBounds(
|
||||||
|
this._mapItems ? this._mapItems.map((item) => item.getLatLng()) : []
|
||||||
|
);
|
||||||
|
this._leafletMap.fitBounds(bounds.pad(0.5));
|
||||||
|
|
||||||
|
if (this.zoom && this._leafletMap.getZoom() > this.zoom) {
|
||||||
|
this._leafletMap.setZoom(this.zoom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _drawEntities(): void {
|
||||||
|
const hass = this.hass;
|
||||||
|
const map = this._leafletMap;
|
||||||
|
const Leaflet = this.Leaflet;
|
||||||
|
if (!hass || !map || !Leaflet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._mapItems) {
|
||||||
|
this._mapItems.forEach((marker) => marker.remove());
|
||||||
|
}
|
||||||
|
const mapItems: Layer[] = (this._mapItems = []);
|
||||||
|
|
||||||
|
if (this._mapZones) {
|
||||||
|
this._mapZones.forEach((marker) => marker.remove());
|
||||||
|
}
|
||||||
|
const mapZones: Layer[] = (this._mapZones = []);
|
||||||
|
|
||||||
|
const allEntities = this.entities!.concat();
|
||||||
|
|
||||||
|
for (const entity of allEntities) {
|
||||||
|
const entityId = entity;
|
||||||
|
const stateObj = hass.states[entityId];
|
||||||
|
if (!stateObj) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const title = computeStateName(stateObj);
|
||||||
|
const {
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
passive,
|
||||||
|
icon,
|
||||||
|
radius,
|
||||||
|
entity_picture: entityPicture,
|
||||||
|
gps_accuracy: gpsAccuracy,
|
||||||
|
} = stateObj.attributes;
|
||||||
|
|
||||||
|
if (!(latitude && longitude)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (computeStateDomain(stateObj) === "zone") {
|
||||||
|
// DRAW ZONE
|
||||||
|
if (passive) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create icon
|
||||||
|
let iconHTML = "";
|
||||||
|
if (icon) {
|
||||||
|
const el = document.createElement("ha-icon");
|
||||||
|
el.setAttribute("icon", icon);
|
||||||
|
iconHTML = el.outerHTML;
|
||||||
|
} else {
|
||||||
|
const el = document.createElement("span");
|
||||||
|
el.innerHTML = title;
|
||||||
|
iconHTML = el.outerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create marker with the icon
|
||||||
|
mapZones.push(
|
||||||
|
Leaflet.marker([latitude, longitude], {
|
||||||
|
icon: Leaflet.divIcon({
|
||||||
|
html: iconHTML,
|
||||||
|
iconSize: [24, 24],
|
||||||
|
className: this.darkMode ? "dark" : "light",
|
||||||
|
}),
|
||||||
|
interactive: false,
|
||||||
|
title,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// create circle around it
|
||||||
|
mapZones.push(
|
||||||
|
Leaflet.circle([latitude, longitude], {
|
||||||
|
interactive: false,
|
||||||
|
color: "#FF9800",
|
||||||
|
radius,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DRAW ENTITY
|
||||||
|
// create icon
|
||||||
|
const entityName = title
|
||||||
|
.split(" ")
|
||||||
|
.map((part) => part[0])
|
||||||
|
.join("")
|
||||||
|
.substr(0, 3);
|
||||||
|
|
||||||
|
// create market with the icon
|
||||||
|
mapItems.push(
|
||||||
|
Leaflet.marker([latitude, longitude], {
|
||||||
|
icon: Leaflet.divIcon({
|
||||||
|
// Leaflet clones this element before adding it to the map. This messes up
|
||||||
|
// our Polymer object and we can't pass data through. Thus we hack like this.
|
||||||
|
html: `
|
||||||
|
<ha-entity-marker
|
||||||
|
entity-id="${entityId}"
|
||||||
|
entity-name="${entityName}"
|
||||||
|
entity-picture="${entityPicture || ""}"
|
||||||
|
></ha-entity-marker>
|
||||||
|
`,
|
||||||
|
iconSize: [48, 48],
|
||||||
|
className: "",
|
||||||
|
}),
|
||||||
|
title: computeStateName(stateObj),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// create circle around if entity has accuracy
|
||||||
|
if (gpsAccuracy) {
|
||||||
|
mapItems.push(
|
||||||
|
Leaflet.circle([latitude, longitude], {
|
||||||
|
interactive: false,
|
||||||
|
color: "#0288D1",
|
||||||
|
radius: gpsAccuracy,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._mapItems.forEach((marker) => map.addLayer(marker));
|
||||||
|
this._mapZones.forEach((marker) => map.addLayer(marker));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _attachObserver(): void {
|
||||||
|
// Observe changes to map size and invalidate to prevent broken rendering
|
||||||
|
// Uses ResizeObserver in Chrome, otherwise window resize event
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
if (typeof ResizeObserver === "function") {
|
||||||
|
// @ts-ignore
|
||||||
|
this._resizeObserver = new ResizeObserver(() =>
|
||||||
|
this._debouncedResizeListener()
|
||||||
|
);
|
||||||
|
this._resizeObserver.observe(this._mapEl);
|
||||||
|
} else {
|
||||||
|
window.addEventListener("resize", this._debouncedResizeListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
#map {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
#map.dark {
|
||||||
|
background: #090909;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-map": HaMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|||||||
import "./entity/ha-chart-base";
|
import "./entity/ha-chart-base";
|
||||||
|
|
||||||
import LocalizeMixin from "../mixins/localize-mixin";
|
import LocalizeMixin from "../mixins/localize-mixin";
|
||||||
import formatDateTime from "../common/datetime/format_date_time";
|
import { formatDateTimeWithSeconds } from "../common/datetime/format_date_time";
|
||||||
|
|
||||||
class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
|
class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
|
||||||
static get template() {
|
static get template() {
|
||||||
@@ -317,7 +317,7 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
|
|||||||
const item = items[0];
|
const item = items[0];
|
||||||
const date = data.datasets[item.datasetIndex].data[item.index].x;
|
const date = data.datasets[item.datasetIndex].data[item.index].x;
|
||||||
|
|
||||||
return formatDateTime(date, this.hass.language);
|
return formatDateTimeWithSeconds(date, this.hass.language);
|
||||||
};
|
};
|
||||||
|
|
||||||
const chartOptions = {
|
const chartOptions = {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import LocalizeMixin from "../mixins/localize-mixin";
|
|||||||
|
|
||||||
import "./entity/ha-chart-base";
|
import "./entity/ha-chart-base";
|
||||||
|
|
||||||
import formatDateTime from "../common/datetime/format_date_time";
|
import { formatDateTimeWithSeconds } from "../common/datetime/format_date_time";
|
||||||
import { computeRTL } from "../common/util/compute_rtl";
|
import { computeRTL } from "../common/util/compute_rtl";
|
||||||
|
|
||||||
class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
|
class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
|
||||||
@@ -165,8 +165,8 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
|
|||||||
const formatTooltipLabel = (item, data) => {
|
const formatTooltipLabel = (item, data) => {
|
||||||
const values = data.datasets[item.datasetIndex].data[item.index];
|
const values = data.datasets[item.datasetIndex].data[item.index];
|
||||||
|
|
||||||
const start = formatDateTime(values[0], this.hass.language);
|
const start = formatDateTimeWithSeconds(values[0], this.hass.language);
|
||||||
const end = formatDateTime(values[1], this.hass.language);
|
const end = formatDateTimeWithSeconds(values[1], this.hass.language);
|
||||||
const state = values[2];
|
const state = values[2];
|
||||||
|
|
||||||
return [state, start, end];
|
return [state, start, end];
|
||||||
|
|||||||
@@ -173,6 +173,12 @@ export type Condition =
|
|||||||
| DeviceCondition
|
| DeviceCondition
|
||||||
| LogicalCondition;
|
| LogicalCondition;
|
||||||
|
|
||||||
|
export const triggerAutomation = (hass: HomeAssistant, entityId: string) => {
|
||||||
|
hass.callService("automation", "trigger", {
|
||||||
|
entity_id: entityId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const deleteAutomation = (hass: HomeAssistant, id: string) =>
|
export const deleteAutomation = (hass: HomeAssistant, id: string) =>
|
||||||
hass.callApi("DELETE", `config/automation/config/${id}`);
|
hass.callApi("DELETE", `config/automation/config/${id}`);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { HomeAssistant } from "../types";
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { LocalizeFunc } from "../common/translations/localize";
|
import { LocalizeFunc } from "../common/translations/localize";
|
||||||
|
|
||||||
interface CacheConfig {
|
export interface CacheConfig {
|
||||||
refresh: number;
|
refresh: number;
|
||||||
cacheKey: string;
|
cacheKey: string;
|
||||||
hoursToShow: number;
|
hoursToShow: number;
|
||||||
|
|||||||
@@ -41,15 +41,6 @@ export const fetchThumbnailUrl = async (
|
|||||||
return hass.hassUrl(path.path);
|
return hass.hassUrl(path.path);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchThumbnail = (hass: HomeAssistant, entityId: string) => {
|
|
||||||
// tslint:disable-next-line: no-console
|
|
||||||
console.warn("This method has been deprecated.");
|
|
||||||
return hass.callWS<CameraThumbnail>({
|
|
||||||
type: "camera_thumbnail",
|
|
||||||
entity_id: entityId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchStreamUrl = async (
|
export const fetchStreamUrl = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityId: string,
|
entityId: string,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { HaFormSchema } from "../components/ha-form/ha-form";
|
||||||
|
|
||||||
export interface DataEntryFlowProgressedEvent {
|
export interface DataEntryFlowProgressedEvent {
|
||||||
type: "data_entry_flow_progressed";
|
type: "data_entry_flow_progressed";
|
||||||
data: {
|
data: {
|
||||||
@@ -7,12 +9,6 @@ export interface DataEntryFlowProgressedEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FieldSchema {
|
|
||||||
name: string;
|
|
||||||
default?: any;
|
|
||||||
optional: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DataEntryFlowProgress {
|
export interface DataEntryFlowProgress {
|
||||||
flow_id: string;
|
flow_id: string;
|
||||||
handler: string;
|
handler: string;
|
||||||
@@ -27,7 +23,7 @@ export interface DataEntryFlowStepForm {
|
|||||||
flow_id: string;
|
flow_id: string;
|
||||||
handler: string;
|
handler: string;
|
||||||
step_id: string;
|
step_id: string;
|
||||||
data_schema: FieldSchema[];
|
data_schema: HaFormSchema[];
|
||||||
errors: { [key: string]: string };
|
errors: { [key: string]: string };
|
||||||
description_placeholders: { [key: string]: string };
|
description_placeholders: { [key: string]: string };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,15 @@ export const fetchDeviceTriggerCapabilities = (
|
|||||||
trigger,
|
trigger,
|
||||||
});
|
});
|
||||||
|
|
||||||
const whitelist = ["above", "below", "code", "for"];
|
const whitelist = [
|
||||||
|
"above",
|
||||||
|
"below",
|
||||||
|
"brightness",
|
||||||
|
"code",
|
||||||
|
"for",
|
||||||
|
"position",
|
||||||
|
"set_brightness",
|
||||||
|
];
|
||||||
|
|
||||||
export const deviceAutomationsEqual = (
|
export const deviceAutomationsEqual = (
|
||||||
a: DeviceAutomation,
|
a: DeviceAutomation,
|
||||||
@@ -99,49 +107,65 @@ export const deviceAutomationsEqual = (
|
|||||||
export const localizeDeviceAutomationAction = (
|
export const localizeDeviceAutomationAction = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
action: DeviceAction
|
action: DeviceAction
|
||||||
) => {
|
): string => {
|
||||||
const state = action.entity_id ? hass.states[action.entity_id] : undefined;
|
const state = action.entity_id ? hass.states[action.entity_id] : undefined;
|
||||||
return hass.localize(
|
return (
|
||||||
`component.${action.domain}.device_automation.action_type.${action.type}`,
|
|
||||||
"entity_name",
|
|
||||||
state ? computeStateName(state) : "<unknown>",
|
|
||||||
"subtype",
|
|
||||||
hass.localize(
|
hass.localize(
|
||||||
`component.${action.domain}.device_automation.action_subtype.${action.subtype}`
|
`component.${action.domain}.device_automation.action_type.${action.type}`,
|
||||||
)
|
"entity_name",
|
||||||
|
state ? computeStateName(state) : action.entity_id || "<unknown>",
|
||||||
|
"subtype",
|
||||||
|
action.subtype
|
||||||
|
? hass.localize(
|
||||||
|
`component.${action.domain}.device_automation.action_subtype.${action.subtype}`
|
||||||
|
) || action.subtype
|
||||||
|
: ""
|
||||||
|
) || (action.subtype ? `"${action.subtype}" ${action.type}` : action.type!)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const localizeDeviceAutomationCondition = (
|
export const localizeDeviceAutomationCondition = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
condition: DeviceCondition
|
condition: DeviceCondition
|
||||||
) => {
|
): string => {
|
||||||
const state = condition.entity_id
|
const state = condition.entity_id
|
||||||
? hass.states[condition.entity_id]
|
? hass.states[condition.entity_id]
|
||||||
: undefined;
|
: undefined;
|
||||||
return hass.localize(
|
return (
|
||||||
`component.${condition.domain}.device_automation.condition_type.${condition.type}`,
|
|
||||||
"entity_name",
|
|
||||||
state ? computeStateName(state) : "<unknown>",
|
|
||||||
"subtype",
|
|
||||||
hass.localize(
|
hass.localize(
|
||||||
`component.${condition.domain}.device_automation.condition_subtype.${condition.subtype}`
|
`component.${condition.domain}.device_automation.condition_type.${condition.type}`,
|
||||||
)
|
"entity_name",
|
||||||
|
state ? computeStateName(state) : condition.entity_id || "<unknown>",
|
||||||
|
"subtype",
|
||||||
|
condition.subtype
|
||||||
|
? hass.localize(
|
||||||
|
`component.${condition.domain}.device_automation.condition_subtype.${condition.subtype}`
|
||||||
|
) || condition.subtype
|
||||||
|
: ""
|
||||||
|
) ||
|
||||||
|
(condition.subtype
|
||||||
|
? `"${condition.subtype}" ${condition.type}`
|
||||||
|
: condition.type!)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const localizeDeviceAutomationTrigger = (
|
export const localizeDeviceAutomationTrigger = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
trigger: DeviceTrigger
|
trigger: DeviceTrigger
|
||||||
) => {
|
): string => {
|
||||||
const state = trigger.entity_id ? hass.states[trigger.entity_id] : undefined;
|
const state = trigger.entity_id ? hass.states[trigger.entity_id] : undefined;
|
||||||
return hass.localize(
|
return (
|
||||||
`component.${trigger.domain}.device_automation.trigger_type.${trigger.type}`,
|
|
||||||
"entity_name",
|
|
||||||
state ? computeStateName(state) : "<unknown>",
|
|
||||||
"subtype",
|
|
||||||
hass.localize(
|
hass.localize(
|
||||||
`component.${trigger.domain}.device_automation.trigger_subtype.${trigger.subtype}`
|
`component.${trigger.domain}.device_automation.trigger_type.${trigger.type}`,
|
||||||
)
|
"entity_name",
|
||||||
|
state ? computeStateName(state) : trigger.entity_id || "<unknown>",
|
||||||
|
"subtype",
|
||||||
|
trigger.subtype
|
||||||
|
? hass.localize(
|
||||||
|
`component.${trigger.domain}.device_automation.trigger_subtype.${trigger.subtype}`
|
||||||
|
) || trigger.subtype
|
||||||
|
: ""
|
||||||
|
) ||
|
||||||
|
(trigger.subtype ? `"${trigger.subtype}" ${trigger.type}` : trigger.type!)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ export interface DeviceRegistryEntry {
|
|||||||
name_by_user?: string;
|
name_by_user?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DeviceEntityLookup {
|
||||||
|
[deviceId: string]: EntityRegistryEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface DeviceRegistryEntryMutableParams {
|
export interface DeviceRegistryEntryMutableParams {
|
||||||
area_id?: string | null;
|
area_id?: string | null;
|
||||||
name_by_user?: string | null;
|
name_by_user?: string | null;
|
||||||
|
|||||||
@@ -6,14 +6,23 @@ import { debounce } from "../common/util/debounce";
|
|||||||
export interface EntityRegistryEntry {
|
export interface EntityRegistryEntry {
|
||||||
entity_id: string;
|
entity_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
icon?: string;
|
||||||
platform: string;
|
platform: string;
|
||||||
config_entry_id?: string;
|
config_entry_id?: string;
|
||||||
device_id?: string;
|
device_id?: string;
|
||||||
disabled_by: string | null;
|
disabled_by: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
|
||||||
|
unique_id: string;
|
||||||
|
capabilities: object;
|
||||||
|
original_name?: string;
|
||||||
|
original_icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EntityRegistryEntryUpdateParams {
|
export interface EntityRegistryEntryUpdateParams {
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
|
icon?: string | null;
|
||||||
disabled_by?: string | null;
|
disabled_by?: string | null;
|
||||||
new_entity_id?: string;
|
new_entity_id?: string;
|
||||||
}
|
}
|
||||||
@@ -29,12 +38,21 @@ export const computeEntityRegistryName = (
|
|||||||
return state ? computeStateName(state) : null;
|
return state ? computeStateName(state) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getExtendedEntityRegistryEntry = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entityId: string
|
||||||
|
): Promise<ExtEntityRegistryEntry> =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "config/entity_registry/get",
|
||||||
|
entity_id: entityId,
|
||||||
|
});
|
||||||
|
|
||||||
export const updateEntityRegistryEntry = (
|
export const updateEntityRegistryEntry = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityId: string,
|
entityId: string,
|
||||||
updates: Partial<EntityRegistryEntryUpdateParams>
|
updates: Partial<EntityRegistryEntryUpdateParams>
|
||||||
): Promise<EntityRegistryEntry> =>
|
): Promise<ExtEntityRegistryEntry> =>
|
||||||
hass.callWS<EntityRegistryEntry>({
|
hass.callWS({
|
||||||
type: "config/entity_registry/update",
|
type: "config/entity_registry/update",
|
||||||
entity_id: entityId,
|
entity_id: entityId,
|
||||||
...updates,
|
...updates,
|
||||||
|
|||||||
4
src/data/external.ts
Normal file
4
src/data/external.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export const isExternal =
|
||||||
|
window.externalApp ||
|
||||||
|
window.webkit?.messageHandlers?.getExternalAuth ||
|
||||||
|
location.search.includes("external_auth=1");
|
||||||
@@ -59,3 +59,12 @@ export const getOptimisticFrontendUserDataCollection = <
|
|||||||
`_frontendUserData-${userDataKey}`,
|
`_frontendUserData-${userDataKey}`,
|
||||||
() => fetchFrontendUserData(conn, userDataKey)
|
() => fetchFrontendUserData(conn, userDataKey)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const subscribeFrontendUserData = <UserDataKey extends ValidUserDataKey>(
|
||||||
|
conn: Connection,
|
||||||
|
userDataKey: UserDataKey,
|
||||||
|
onChange: (state: FrontendUserData[UserDataKey] | null) => void
|
||||||
|
) =>
|
||||||
|
getOptimisticFrontendUserDataCollection(conn, userDataKey).subscribe(
|
||||||
|
onChange
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { HomeAssistant } from "../types";
|
|
||||||
|
|
||||||
export const setInputSelectOption = (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entity: string,
|
|
||||||
option: string
|
|
||||||
) =>
|
|
||||||
hass.callService("input_select", "select_option", {
|
|
||||||
option,
|
|
||||||
entity_id: entity,
|
|
||||||
});
|
|
||||||
43
src/data/input_boolean.ts
Normal file
43
src/data/input_boolean.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
|
export interface InputBoolean {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
icon?: string;
|
||||||
|
initial?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InputBooleanMutableParams {
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
initial: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchInputBoolean = (hass: HomeAssistant) =>
|
||||||
|
hass.callWS<InputBoolean[]>({ type: "input_boolean/list" });
|
||||||
|
|
||||||
|
export const createInputBoolean = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
values: InputBooleanMutableParams
|
||||||
|
) =>
|
||||||
|
hass.callWS<InputBoolean>({
|
||||||
|
type: "input_boolean/create",
|
||||||
|
...values,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateInputBoolean = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
id: string,
|
||||||
|
updates: Partial<InputBooleanMutableParams>
|
||||||
|
) =>
|
||||||
|
hass.callWS<InputBoolean>({
|
||||||
|
type: "input_boolean/update",
|
||||||
|
input_boolean_id: id,
|
||||||
|
...updates,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteInputBoolean = (hass: HomeAssistant, id: string) =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "input_boolean/delete",
|
||||||
|
input_boolean_id: id,
|
||||||
|
});
|
||||||
@@ -1,5 +1,22 @@
|
|||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
|
export interface InputDateTime {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
icon?: string;
|
||||||
|
initial?: string;
|
||||||
|
has_time: boolean;
|
||||||
|
has_date: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InputDateTimeMutableParams {
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
initial: string;
|
||||||
|
has_time: boolean;
|
||||||
|
has_date: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const setInputDateTimeValue = (
|
export const setInputDateTimeValue = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityId: string,
|
entityId: string,
|
||||||
@@ -9,3 +26,32 @@ export const setInputDateTimeValue = (
|
|||||||
const param = { entity_id: entityId, time, date };
|
const param = { entity_id: entityId, time, date };
|
||||||
hass.callService(entityId.split(".", 1)[0], "set_datetime", param);
|
hass.callService(entityId.split(".", 1)[0], "set_datetime", param);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchInputDateTime = (hass: HomeAssistant) =>
|
||||||
|
hass.callWS<InputDateTime[]>({ type: "input_datetime/list" });
|
||||||
|
|
||||||
|
export const createInputDateTime = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
values: InputDateTimeMutableParams
|
||||||
|
) =>
|
||||||
|
hass.callWS<InputDateTime>({
|
||||||
|
type: "input_datetime/create",
|
||||||
|
...values,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateInputDateTime = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
id: string,
|
||||||
|
updates: Partial<InputDateTimeMutableParams>
|
||||||
|
) =>
|
||||||
|
hass.callWS<InputDateTime>({
|
||||||
|
type: "input_datetime/update",
|
||||||
|
input_datetime_id: id,
|
||||||
|
...updates,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteInputDateTime = (hass: HomeAssistant, id: string) =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "input_datetime/delete",
|
||||||
|
input_datetime_id: id,
|
||||||
|
});
|
||||||
|
|||||||
53
src/data/input_number.ts
Normal file
53
src/data/input_number.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
|
export interface InputNumber {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
icon?: string;
|
||||||
|
initial?: number;
|
||||||
|
step?: number;
|
||||||
|
mode?: "box" | "slider";
|
||||||
|
unit_of_measurement?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InputNumberMutableParams {
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
initial: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step: number;
|
||||||
|
mode: "box" | "slider";
|
||||||
|
unit_of_measurement?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchInputNumber = (hass: HomeAssistant) =>
|
||||||
|
hass.callWS<InputNumber[]>({ type: "input_number/list" });
|
||||||
|
|
||||||
|
export const createInputNumber = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
values: InputNumberMutableParams
|
||||||
|
) =>
|
||||||
|
hass.callWS<InputNumber>({
|
||||||
|
type: "input_number/create",
|
||||||
|
...values,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateInputNumber = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
id: string,
|
||||||
|
updates: Partial<InputNumberMutableParams>
|
||||||
|
) =>
|
||||||
|
hass.callWS<InputNumber>({
|
||||||
|
type: "input_number/update",
|
||||||
|
input_number_id: id,
|
||||||
|
...updates,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteInputNumber = (hass: HomeAssistant, id: string) =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "input_number/delete",
|
||||||
|
input_number_id: id,
|
||||||
|
});
|
||||||
55
src/data/input_select.ts
Normal file
55
src/data/input_select.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
|
export interface InputSelect {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
options: string[];
|
||||||
|
icon?: string;
|
||||||
|
initial?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InputSelectMutableParams {
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
initial: string;
|
||||||
|
options: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setInputSelectOption = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity: string,
|
||||||
|
option: string
|
||||||
|
) =>
|
||||||
|
hass.callService("input_select", "select_option", {
|
||||||
|
option,
|
||||||
|
entity_id: entity,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchInputSelect = (hass: HomeAssistant) =>
|
||||||
|
hass.callWS<InputSelect[]>({ type: "input_select/list" });
|
||||||
|
|
||||||
|
export const createInputSelect = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
values: InputSelectMutableParams
|
||||||
|
) =>
|
||||||
|
hass.callWS<InputSelect>({
|
||||||
|
type: "input_select/create",
|
||||||
|
...values,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateInputSelect = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
id: string,
|
||||||
|
updates: Partial<InputSelectMutableParams>
|
||||||
|
) =>
|
||||||
|
hass.callWS<InputSelect>({
|
||||||
|
type: "input_select/update",
|
||||||
|
input_select_id: id,
|
||||||
|
...updates,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteInputSelect = (hass: HomeAssistant, id: string) =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "input_select/delete",
|
||||||
|
input_select_id: id,
|
||||||
|
});
|
||||||
@@ -1,7 +1,57 @@
|
|||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
|
export interface InputText {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
icon?: string;
|
||||||
|
initial?: string;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
pattern?: string;
|
||||||
|
mode?: "text" | "password";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InputTextMutableParams {
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
initial: string;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
pattern: string;
|
||||||
|
mode: "text" | "password";
|
||||||
|
}
|
||||||
|
|
||||||
export const setValue = (hass: HomeAssistant, entity: string, value: string) =>
|
export const setValue = (hass: HomeAssistant, entity: string, value: string) =>
|
||||||
hass.callService(entity.split(".", 1)[0], "set_value", {
|
hass.callService(entity.split(".", 1)[0], "set_value", {
|
||||||
value,
|
value,
|
||||||
entity_id: entity,
|
entity_id: entity,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const fetchInputText = (hass: HomeAssistant) =>
|
||||||
|
hass.callWS<InputText[]>({ type: "input_text/list" });
|
||||||
|
|
||||||
|
export const createInputText = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
values: InputTextMutableParams
|
||||||
|
) =>
|
||||||
|
hass.callWS<InputText>({
|
||||||
|
type: "input_text/create",
|
||||||
|
...values,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateInputText = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
id: string,
|
||||||
|
updates: Partial<InputTextMutableParams>
|
||||||
|
) =>
|
||||||
|
hass.callWS<InputText>({
|
||||||
|
type: "input_text/update",
|
||||||
|
input_text_id: id,
|
||||||
|
...updates,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteInputText = (hass: HomeAssistant, id: string) =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "input_text/delete",
|
||||||
|
input_text_id: id,
|
||||||
|
});
|
||||||
|
|||||||
10
src/data/integration.ts
Normal file
10
src/data/integration.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { LocalizeFunc } from "../common/translations/localize";
|
||||||
|
|
||||||
|
export const integrationDocsUrl = (domain: string) =>
|
||||||
|
`https://www.home-assistant.io/integrations/${domain}`;
|
||||||
|
|
||||||
|
export const integrationIssuesUrl = (domain: string) =>
|
||||||
|
`https://github.com/home-assistant/home-assistant/issues?q=is%3Aissue+is%3Aopen+label%3A%22integration%3A+${domain}%22`;
|
||||||
|
|
||||||
|
export const domainToName = (localize: LocalizeFunc, domain: string) =>
|
||||||
|
localize(`domain.${domain}`) || domain;
|
||||||
@@ -1,12 +1,69 @@
|
|||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import { Connection, getCollection } from "home-assistant-js-websocket";
|
import {
|
||||||
|
Connection,
|
||||||
|
getCollection,
|
||||||
|
HassEventBase,
|
||||||
|
} from "home-assistant-js-websocket";
|
||||||
import { HASSDomEvent } from "../common/dom/fire_event";
|
import { HASSDomEvent } from "../common/dom/fire_event";
|
||||||
|
|
||||||
|
export interface LovelacePanelConfig {
|
||||||
|
mode: "yaml" | "storage";
|
||||||
|
}
|
||||||
|
|
||||||
export interface LovelaceConfig {
|
export interface LovelaceConfig {
|
||||||
title?: string;
|
title?: string;
|
||||||
views: LovelaceViewConfig[];
|
views: LovelaceViewConfig[];
|
||||||
background?: string;
|
background?: string;
|
||||||
resources?: Array<{ type: "css" | "js" | "module" | "html"; url: string }>;
|
}
|
||||||
|
|
||||||
|
export interface LegacyLovelaceConfig extends LovelaceConfig {
|
||||||
|
resources?: LovelaceResource[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LovelaceResource {
|
||||||
|
id: string;
|
||||||
|
type: "css" | "js" | "module" | "html";
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LovelaceResourcesMutableParams {
|
||||||
|
res_type: "css" | "js" | "module" | "html";
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LovelaceDashboard =
|
||||||
|
| LovelaceYamlDashboard
|
||||||
|
| LovelaceStorageDashboard;
|
||||||
|
|
||||||
|
interface LovelaceGenericDashboard {
|
||||||
|
id: string;
|
||||||
|
url_path: string;
|
||||||
|
require_admin: boolean;
|
||||||
|
show_in_sidebar: boolean;
|
||||||
|
icon?: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LovelaceYamlDashboard extends LovelaceGenericDashboard {
|
||||||
|
mode: "yaml";
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LovelaceStorageDashboard extends LovelaceGenericDashboard {
|
||||||
|
mode: "storage";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LovelaceDashboardMutableParams {
|
||||||
|
require_admin: boolean;
|
||||||
|
show_in_sidebar: boolean;
|
||||||
|
icon?: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LovelaceDashboardCreateParams
|
||||||
|
extends LovelaceDashboardMutableParams {
|
||||||
|
url_path: string;
|
||||||
|
mode: "storage";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LovelaceViewConfig {
|
export interface LovelaceViewConfig {
|
||||||
@@ -95,7 +152,138 @@ export type ActionConfig =
|
|||||||
| NoActionConfig
|
| NoActionConfig
|
||||||
| CustomActionConfig;
|
| CustomActionConfig;
|
||||||
|
|
||||||
|
type LovelaceUpdatedEvent = HassEventBase & {
|
||||||
|
event_type: "lovelace_updated";
|
||||||
|
data: {
|
||||||
|
url_path: string | null;
|
||||||
|
mode: "yaml" | "storage";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchResources = (conn: Connection): Promise<LovelaceResource[]> =>
|
||||||
|
conn.sendMessagePromise({
|
||||||
|
type: "lovelace/resources",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createResource = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
values: LovelaceResourcesMutableParams
|
||||||
|
) =>
|
||||||
|
hass.callWS<LovelaceResource>({
|
||||||
|
type: "lovelace/resources/create",
|
||||||
|
...values,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateResource = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
id: string,
|
||||||
|
updates: Partial<LovelaceResourcesMutableParams>
|
||||||
|
) =>
|
||||||
|
hass.callWS<LovelaceResource>({
|
||||||
|
type: "lovelace/resources/update",
|
||||||
|
resource_id: id,
|
||||||
|
...updates,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteResource = (hass: HomeAssistant, id: string) =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "lovelace/resources/delete",
|
||||||
|
resource_id: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchDashboards = (
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Promise<LovelaceDashboard[]> =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "lovelace/dashboards/list",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createDashboard = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
values: LovelaceDashboardCreateParams
|
||||||
|
) =>
|
||||||
|
hass.callWS<LovelaceDashboard>({
|
||||||
|
type: "lovelace/dashboards/create",
|
||||||
|
...values,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateDashboard = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
id: string,
|
||||||
|
updates: Partial<LovelaceDashboardMutableParams>
|
||||||
|
) =>
|
||||||
|
hass.callWS<LovelaceDashboard>({
|
||||||
|
type: "lovelace/dashboards/update",
|
||||||
|
dashboard_id: id,
|
||||||
|
...updates,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteDashboard = (hass: HomeAssistant, id: string) =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "lovelace/dashboards/delete",
|
||||||
|
dashboard_id: id,
|
||||||
|
});
|
||||||
|
|
||||||
export const fetchConfig = (
|
export const fetchConfig = (
|
||||||
|
conn: Connection,
|
||||||
|
urlPath: string | null,
|
||||||
|
force: boolean
|
||||||
|
): Promise<LovelaceConfig> =>
|
||||||
|
conn.sendMessagePromise({
|
||||||
|
type: "lovelace/config",
|
||||||
|
url_path: urlPath,
|
||||||
|
force,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const saveConfig = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
urlPath: string | null,
|
||||||
|
config: LovelaceConfig
|
||||||
|
): Promise<void> =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "lovelace/config/save",
|
||||||
|
url_path: urlPath,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteConfig = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
urlPath: string | null
|
||||||
|
): Promise<void> =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "lovelace/config/delete",
|
||||||
|
url_path: urlPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const subscribeLovelaceUpdates = (
|
||||||
|
conn: Connection,
|
||||||
|
urlPath: string | null,
|
||||||
|
onChange: () => void
|
||||||
|
) =>
|
||||||
|
conn.subscribeEvents<LovelaceUpdatedEvent>((ev) => {
|
||||||
|
if (ev.data.url_path === urlPath) {
|
||||||
|
onChange();
|
||||||
|
}
|
||||||
|
}, "lovelace_updated");
|
||||||
|
|
||||||
|
export const getLovelaceCollection = (
|
||||||
|
conn: Connection,
|
||||||
|
urlPath: string | null = null
|
||||||
|
) =>
|
||||||
|
getCollection(
|
||||||
|
conn,
|
||||||
|
`_lovelace_${urlPath ?? ""}`,
|
||||||
|
(conn2) => fetchConfig(conn2, urlPath, false),
|
||||||
|
(_conn, store) =>
|
||||||
|
subscribeLovelaceUpdates(conn, urlPath, () =>
|
||||||
|
fetchConfig(conn, urlPath, false).then((config) =>
|
||||||
|
store.setState(config, true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Legacy functions to support cast for Home Assistion < 0.107
|
||||||
|
const fetchLegacyConfig = (
|
||||||
conn: Connection,
|
conn: Connection,
|
||||||
force: boolean
|
force: boolean
|
||||||
): Promise<LovelaceConfig> =>
|
): Promise<LovelaceConfig> =>
|
||||||
@@ -104,38 +292,27 @@ export const fetchConfig = (
|
|||||||
force,
|
force,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const saveConfig = (
|
const subscribeLegacyLovelaceUpdates = (
|
||||||
hass: HomeAssistant,
|
|
||||||
config: LovelaceConfig
|
|
||||||
): Promise<void> =>
|
|
||||||
hass.callWS({
|
|
||||||
type: "lovelace/config/save",
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const deleteConfig = (hass: HomeAssistant): Promise<void> =>
|
|
||||||
hass.callWS({
|
|
||||||
type: "lovelace/config/delete",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const subscribeLovelaceUpdates = (
|
|
||||||
conn: Connection,
|
conn: Connection,
|
||||||
onChange: () => void
|
onChange: () => void
|
||||||
) => conn.subscribeEvents(onChange, "lovelace_updated");
|
) => conn.subscribeEvents(onChange, "lovelace_updated");
|
||||||
|
|
||||||
export const getLovelaceCollection = (conn: Connection) =>
|
export const getLegacyLovelaceCollection = (conn: Connection) =>
|
||||||
getCollection(
|
getCollection(
|
||||||
conn,
|
conn,
|
||||||
"_lovelace",
|
"_lovelace",
|
||||||
(conn2) => fetchConfig(conn2, false),
|
(conn2) => fetchLegacyConfig(conn2, false),
|
||||||
(_conn, store) =>
|
(_conn, store) =>
|
||||||
subscribeLovelaceUpdates(conn, () =>
|
subscribeLegacyLovelaceUpdates(conn, () =>
|
||||||
fetchConfig(conn, false).then((config) => store.setState(config, true))
|
fetchLegacyConfig(conn, false).then((config) =>
|
||||||
|
store.setState(config, true)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface WindowWithLovelaceProm extends Window {
|
export interface WindowWithLovelaceProm extends Window {
|
||||||
llConfProm?: Promise<LovelaceConfig>;
|
llConfProm?: Promise<LovelaceConfig>;
|
||||||
|
llResProm?: Promise<LovelaceResource[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionHandlerOptions {
|
export interface ActionHandlerOptions {
|
||||||
|
|||||||
@@ -1,35 +1,61 @@
|
|||||||
import { HomeAssistant } from "../types";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
|
||||||
import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise";
|
|
||||||
|
|
||||||
export const SUPPORT_PAUSE = 1;
|
export const SUPPORT_PAUSE = 1;
|
||||||
|
export const SUPPORT_SEEK = 2;
|
||||||
|
export const SUPPORT_VOLUME_SET = 4;
|
||||||
|
export const SUPPORT_VOLUME_MUTE = 8;
|
||||||
|
export const SUPPORT_PREVIOUS_TRACK = 16;
|
||||||
export const SUPPORT_NEXT_TRACK = 32;
|
export const SUPPORT_NEXT_TRACK = 32;
|
||||||
|
export const SUPPORT_TURN_ON = 128;
|
||||||
|
export const SUPPORT_TURN_OFF = 256;
|
||||||
|
export const SUPPORT_PLAY_MEDIA = 512;
|
||||||
|
export const SUPPORT_VOLUME_BUTTONS = 1024;
|
||||||
|
export const SUPPORT_SELECT_SOURCE = 2048;
|
||||||
|
export const SUPPORT_STOP = 4096;
|
||||||
export const SUPPORTS_PLAY = 16384;
|
export const SUPPORTS_PLAY = 16384;
|
||||||
|
export const SUPPORT_SELECT_SOUND_MODE = 65536;
|
||||||
export const OFF_STATES = ["off", "idle"];
|
export const OFF_STATES = ["off", "idle"];
|
||||||
|
export const CONTRAST_RATIO = 3.5;
|
||||||
|
|
||||||
export interface MediaPlayerThumbnail {
|
export interface MediaPlayerThumbnail {
|
||||||
content_type: string;
|
content_type: string;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchMediaPlayerThumbnailWithCache = (
|
export const getCurrentProgress = (stateObj: HassEntity): number => {
|
||||||
hass: HomeAssistant,
|
let progress = stateObj.attributes.media_position;
|
||||||
entityId: string
|
progress +=
|
||||||
) =>
|
(Date.now() -
|
||||||
timeCachePromiseFunc(
|
new Date(stateObj.attributes.media_position_updated_at).getTime()) /
|
||||||
"_media_playerTmb",
|
1000.0;
|
||||||
9000,
|
return progress;
|
||||||
fetchMediaPlayerThumbnail,
|
};
|
||||||
hass,
|
|
||||||
entityId
|
export const computeMediaDescription = (stateObj: HassEntity): string => {
|
||||||
);
|
let secondaryTitle: string;
|
||||||
|
|
||||||
export const fetchMediaPlayerThumbnail = (
|
switch (stateObj.attributes.media_content_type) {
|
||||||
hass: HomeAssistant,
|
case "music":
|
||||||
entityId: string
|
secondaryTitle = stateObj.attributes.media_artist;
|
||||||
) => {
|
break;
|
||||||
return hass.callWS<MediaPlayerThumbnail>({
|
case "playlist":
|
||||||
type: "media_player_thumbnail",
|
secondaryTitle = stateObj.attributes.media_playlist;
|
||||||
entity_id: entityId,
|
break;
|
||||||
});
|
case "tvshow":
|
||||||
|
secondaryTitle = stateObj.attributes.media_series_title;
|
||||||
|
if (stateObj.attributes.media_season) {
|
||||||
|
secondaryTitle += " S" + stateObj.attributes.media_season;
|
||||||
|
|
||||||
|
if (stateObj.attributes.media_episode) {
|
||||||
|
secondaryTitle += "E" + stateObj.attributes.media_episode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
secondaryTitle = stateObj.attributes.app_name
|
||||||
|
? stateObj.attributes.app_name
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return secondaryTitle;
|
||||||
};
|
};
|
||||||
|
|||||||
2
src/data/sensor.ts
Normal file
2
src/data/sensor.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const SENSOR_DEVICE_CLASS_BATTERY = "battery";
|
||||||
|
export const SENSOR_DEVICE_CLASS_TIMESTAMP = "timestamp";
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
export interface LoggedError {
|
export interface LoggedError {
|
||||||
message: string;
|
name: string;
|
||||||
|
message: [string];
|
||||||
level: string;
|
level: string;
|
||||||
source: string;
|
source: [string, number];
|
||||||
// unix timestamp in seconds
|
// unix timestamp in seconds
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
exception: string;
|
exception: string;
|
||||||
@@ -14,3 +15,8 @@ export interface LoggedError {
|
|||||||
|
|
||||||
export const fetchSystemLog = (hass: HomeAssistant) =>
|
export const fetchSystemLog = (hass: HomeAssistant) =>
|
||||||
hass.callApi<LoggedError[]>("GET", "error/all");
|
hass.callApi<LoggedError[]>("GET", "error/all");
|
||||||
|
|
||||||
|
export const getLoggedErrorIntegration = (item: LoggedError) =>
|
||||||
|
item.name.startsWith("homeassistant.components.")
|
||||||
|
? item.name.split(".")[2]
|
||||||
|
: undefined;
|
||||||
|
|||||||
22
src/data/vacuum.ts
Normal file
22
src/data/vacuum.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import {
|
||||||
|
HassEntityAttributeBase,
|
||||||
|
HassEntityBase,
|
||||||
|
} from "home-assistant-js-websocket";
|
||||||
|
|
||||||
|
export const VACUUM_SUPPORT_PAUSE = 4;
|
||||||
|
export const VACUUM_SUPPORT_STOP = 8;
|
||||||
|
export const VACUUM_SUPPORT_RETURN_HOME = 16;
|
||||||
|
export const VACUUM_SUPPORT_FAN_SPEED = 32;
|
||||||
|
export const VACUUM_SUPPORT_BATTERY = 64;
|
||||||
|
export const VACUUM_SUPPORT_STATUS = 128;
|
||||||
|
export const VACUUM_SUPPORT_LOCATE = 512;
|
||||||
|
export const VACUUM_SUPPORT_CLEAN_SPOT = 1024;
|
||||||
|
export const VACUUM_SUPPORT_START = 8192;
|
||||||
|
|
||||||
|
export type VacuumEntity = HassEntityBase & {
|
||||||
|
attributes: HassEntityAttributeBase & {
|
||||||
|
battery_level: number;
|
||||||
|
fan_speed: any;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -8,7 +8,7 @@ const fetchThemes = (conn) =>
|
|||||||
|
|
||||||
const subscribeUpdates = (conn, store) =>
|
const subscribeUpdates = (conn, store) =>
|
||||||
conn.subscribeEvents(
|
conn.subscribeEvents(
|
||||||
(event) => store.setState(event.data, true),
|
() => fetchThemes(conn).then((data) => store.setState(data, true)),
|
||||||
"themes_updated"
|
"themes_updated"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
|
import { navigate } from "../common/navigate";
|
||||||
|
|
||||||
export const defaultRadiusColor = "#FF9800";
|
export const defaultRadiusColor = "#FF9800";
|
||||||
export const homeRadiusColor: string = "#03a9f4";
|
export const homeRadiusColor: string = "#03a9f4";
|
||||||
@@ -48,3 +49,19 @@ export const deleteZone = (hass: HomeAssistant, zoneId: string) =>
|
|||||||
type: "zone/delete",
|
type: "zone/delete",
|
||||||
zone_id: zoneId,
|
zone_id: zoneId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let inititialZoneEditorData: Partial<ZoneMutableParams> | undefined;
|
||||||
|
|
||||||
|
export const showZoneEditor = (
|
||||||
|
el: HTMLElement,
|
||||||
|
data?: Partial<ZoneMutableParams>
|
||||||
|
) => {
|
||||||
|
inititialZoneEditorData = data;
|
||||||
|
navigate(el, "/config/zone/new");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getZoneEditorInitData = () => {
|
||||||
|
const data = inititialZoneEditorData;
|
||||||
|
inititialZoneEditorData = undefined;
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user