mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-10 17:59:25 +00:00
Compare commits
253 Commits
20200130.3
...
20200311.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b12f29afad | ||
![]() |
04b23388b5 | ||
![]() |
7309a937e8 | ||
![]() |
5dbcd1f726 | ||
![]() |
3338459139 | ||
![]() |
35f17fc1d4 | ||
![]() |
e062940639 | ||
![]() |
ff4d5265c5 | ||
![]() |
906f417436 | ||
![]() |
1c75fe3bb8 | ||
![]() |
283e858576 | ||
![]() |
2b4ab6320b | ||
![]() |
2085260ce7 | ||
![]() |
1f143176ad | ||
![]() |
dd2163a837 | ||
![]() |
0e1eca8a3e | ||
![]() |
9da32880ec | ||
![]() |
15aee6a66a | ||
![]() |
aba74f074a | ||
![]() |
75860508de | ||
![]() |
52160a367a | ||
![]() |
5651a61604 | ||
![]() |
959d8c3181 | ||
![]() |
64ee7456dc | ||
![]() |
814fcf63a8 | ||
![]() |
b72d8cf7d7 | ||
![]() |
8e7ef58715 | ||
![]() |
56bfa01c56 | ||
![]() |
4a0fc3e087 | ||
![]() |
f3c371996f | ||
![]() |
e5467181cb | ||
![]() |
0b3d2ea4ad | ||
![]() |
9648aa3588 | ||
![]() |
1b92cbbf74 | ||
![]() |
9979c046b3 | ||
![]() |
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 | ||
![]() |
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 | ||
![]() |
2e4c73c087 | ||
![]() |
c7f7ef28bf | ||
![]() |
aac7dbab58 | ||
![]() |
8518f774d4 | ||
![]() |
cb0d91d124 | ||
![]() |
107f428dd3 | ||
![]() |
7758ddba56 | ||
![]() |
e0376c803f | ||
![]() |
788c490bbc | ||
![]() |
cdf6e9eb75 | ||
![]() |
4aa49f66bc | ||
![]() |
1bf82f216a | ||
![]() |
004ff58c21 | ||
![]() |
f1a1654371 | ||
![]() |
862044ca23 | ||
![]() |
c54b474838 | ||
![]() |
42cbe863bb | ||
![]() |
ccc42dad79 | ||
![]() |
82ff444cec | ||
![]() |
24c591fbf3 | ||
![]() |
ad676d7fd3 | ||
![]() |
cbe4782d78 | ||
![]() |
3fdcc1c0ea | ||
![]() |
f9d64e51c4 | ||
![]() |
b082828a75 | ||
![]() |
25f5bf0042 | ||
![]() |
f5dec3c6d5 | ||
![]() |
3215437bb8 | ||
![]() |
33176d8f3d | ||
![]() |
f82b62f45c | ||
![]() |
edfdd0da89 | ||
![]() |
33d9bf4660 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -11,3 +11,4 @@
|
||||
*.mp3 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.
|
||||
labels: bug
|
||||
---
|
||||
|
||||
<!-- READ THIS FIRST:
|
||||
- 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
|
||||
@@ -10,6 +11,7 @@ labels: bug
|
||||
- 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.
|
||||
-->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] 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.
|
||||
|
||||
## The problem
|
||||
|
||||
<!--
|
||||
Describe the issue you are experiencing here to communicate to the
|
||||
maintainers. Tell us about the current behavior.
|
||||
If possible provide a screenshot with a description.
|
||||
-->
|
||||
|
||||
|
||||
## Expected behavior
|
||||
<!--
|
||||
|
||||
<!--
|
||||
Describe what you expected to happen or it should look/behave.
|
||||
If possible provide a screenshot with a description.
|
||||
-->
|
||||
|
||||
|
||||
## Steps to reproduce
|
||||
|
||||
<!--
|
||||
Provide steps for us, that helps reproducing your issue.
|
||||
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
|
||||
-->
|
||||
|
||||
|
||||
## Environment
|
||||
|
||||
<!--
|
||||
Provide details about the versions you are using, which helps us reproducing
|
||||
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.
|
||||
-->
|
||||
|
||||
- Home Assistant release with the issue:
|
||||
- Last working Home Assistant release (if known):
|
||||
- UI Type (States or Lovelace):
|
||||
- Browser and browser version:
|
||||
- Operating system:
|
||||
- Home Assistant release with the issue:
|
||||
- Last working Home Assistant release (if known):
|
||||
- Browser and browser version:
|
||||
- Operating system:
|
||||
|
||||
## Problem-relevant configuration
|
||||
|
||||
<!--
|
||||
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
|
||||
@@ -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
|
||||
|
||||
<!--
|
||||
If you come across any javascript or other error logs, e.g., in your browser
|
||||
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
|
||||
|
||||
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,7 +1,7 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- 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.
|
||||
- name: Report incorrect or missing information on our website
|
||||
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.
|
||||
|
||||
@@ -19,12 +19,15 @@ This is the repository for the official [Home Assistant](https://home-assistant.
|
||||
## Frontend development
|
||||
|
||||
### 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.
|
||||
|
||||
### 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:
|
||||
* `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
|
||||
|
||||
|
@@ -11,7 +11,7 @@ trigger:
|
||||
pr: none
|
||||
schedules:
|
||||
- cron: "30 0 * * *"
|
||||
displayName: "translation update"
|
||||
displayName: "frontend translation update"
|
||||
branches:
|
||||
include:
|
||||
- dev
|
||||
|
@@ -34,6 +34,7 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
|
||||
},
|
||||
],
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
[
|
||||
require("@babel/plugin-proposal-decorators").default,
|
||||
{ decoratorsBeforeExport: true },
|
||||
|
@@ -24,7 +24,7 @@ gulp.task(
|
||||
gulp.parallel("gen-icons-app", "gen-icons-mdi"),
|
||||
"gen-pages-dev",
|
||||
"gen-index-app-dev",
|
||||
gulp.series("create-test-translation", "build-translations")
|
||||
"build-translations"
|
||||
),
|
||||
"copy-static",
|
||||
"webpack-watch-app"
|
||||
|
@@ -16,7 +16,7 @@ gulp.task(
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-gallery",
|
||||
gulp.parallel("gen-icons-app", "gen-icons-app", "build-translations"),
|
||||
gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
|
||||
"copy-static-gallery",
|
||||
"gen-index-gallery-dev",
|
||||
"webpack-dev-server-gallery"
|
||||
|
@@ -65,6 +65,12 @@ function copyMapPanel(staticDir) {
|
||||
);
|
||||
}
|
||||
|
||||
gulp.task("copy-translations", (done) => {
|
||||
const staticDir = paths.static;
|
||||
copyTranslations(staticDir);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("copy-static", (done) => {
|
||||
const staticDir = paths.static;
|
||||
const staticPath = genStaticPath(paths.static);
|
||||
|
@@ -2,6 +2,7 @@ const gulp = require("gulp");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const paths = require("../paths");
|
||||
const { mapFiles } = require("../util");
|
||||
|
||||
const ICON_PACKAGE_PATH = path.resolve(
|
||||
__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>`;
|
||||
}
|
||||
|
||||
// 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.
|
||||
function findIcons(searchPath, iconsetName) {
|
||||
const iconRegex = new RegExp(`${iconsetName}:[\\w-]+`, "g");
|
||||
|
@@ -1,14 +1,18 @@
|
||||
const crypto = require("crypto");
|
||||
const del = require("del");
|
||||
const path = require("path");
|
||||
const source = require("vinyl-source-stream");
|
||||
const vinylBuffer = require("vinyl-buffer");
|
||||
const gulp = require("gulp");
|
||||
const fs = require("fs");
|
||||
const foreach = require("gulp-foreach");
|
||||
const hash = require("gulp-hash");
|
||||
const hashFilename = require("gulp-hash-filename");
|
||||
const merge = require("gulp-merge-json");
|
||||
const minify = require("gulp-jsonminify");
|
||||
const rename = require("gulp-rename");
|
||||
const transform = require("gulp-json-transform");
|
||||
const { mapFiles } = require("../util");
|
||||
const env = require("../env");
|
||||
const paths = require("../paths");
|
||||
|
||||
const inDir = "translations";
|
||||
const workDir = "build-translations";
|
||||
@@ -39,8 +43,6 @@ const TRANSLATION_FRAGMENTS = [
|
||||
"developer-tools",
|
||||
];
|
||||
|
||||
const tasks = [];
|
||||
|
||||
function recursiveFlatten(prefix, data) {
|
||||
let output = {};
|
||||
Object.keys(data).forEach(function(key) {
|
||||
@@ -116,11 +118,9 @@ function lokaliseTransform(data, original, file) {
|
||||
return output;
|
||||
}
|
||||
|
||||
let taskName = "clean-translations";
|
||||
gulp.task(taskName, function() {
|
||||
return del([`${outDir}/**/*.json`]);
|
||||
gulp.task("clean-translations", function() {
|
||||
return del([workDir]);
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
||||
gulp.task("ensure-translations-build-dir", (done) => {
|
||||
if (!fs.existsSync(workDir)) {
|
||||
@@ -129,29 +129,23 @@ gulp.task("ensure-translations-build-dir", (done) => {
|
||||
done();
|
||||
});
|
||||
|
||||
taskName = "create-test-metadata";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("ensure-translations-build-dir", function writeTestMetaData(cb) {
|
||||
fs.writeFile(
|
||||
workDir + "/testMetadata.json",
|
||||
JSON.stringify({
|
||||
test: {
|
||||
nativeName: "Test",
|
||||
},
|
||||
}),
|
||||
cb
|
||||
);
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
gulp.task("create-test-metadata", function(cb) {
|
||||
fs.writeFile(
|
||||
workDir + "/testMetadata.json",
|
||||
JSON.stringify({
|
||||
test: {
|
||||
nativeName: "Test",
|
||||
},
|
||||
}),
|
||||
cb
|
||||
);
|
||||
});
|
||||
|
||||
taskName = "create-test-translation";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("create-test-metadata", function() {
|
||||
"create-test-translation",
|
||||
gulp.series("create-test-metadata", function createTestTranslation() {
|
||||
return gulp
|
||||
.src("src/translations/en.json")
|
||||
.src(path.join(paths.translations_src, "en.json"))
|
||||
.pipe(
|
||||
transform(function(data, file) {
|
||||
return recursiveEmpty(data);
|
||||
@@ -161,7 +155,6 @@ gulp.task(
|
||||
.pipe(gulp.dest(workDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
|
||||
/**
|
||||
* 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
|
||||
* the Lokalise update to translations/en.json will not happen immediately.
|
||||
*/
|
||||
taskName = "build-master-translation";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("clean-translations", function() {
|
||||
return gulp
|
||||
.src("src/translations/en.json")
|
||||
.pipe(
|
||||
transform(function(data, file) {
|
||||
return lokaliseTransform(data, data, file);
|
||||
})
|
||||
)
|
||||
.pipe(rename("translationMaster.json"))
|
||||
.pipe(gulp.dest(workDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
gulp.task("build-master-translation", function() {
|
||||
return gulp
|
||||
.src(path.join(paths.translations_src, "en.json"))
|
||||
.pipe(
|
||||
transform(function(data, file) {
|
||||
return lokaliseTransform(data, data, file);
|
||||
})
|
||||
)
|
||||
.pipe(rename("translationMaster.json"))
|
||||
.pipe(gulp.dest(workDir));
|
||||
});
|
||||
|
||||
taskName = "build-merged-translations";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("build-master-translation", function() {
|
||||
return gulp
|
||||
.src([inDir + "/*.json", workDir + "/test.json"], { allowEmpty: true })
|
||||
.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
|
||||
// translation as a failsafe for untranslated strings, and merges all parent
|
||||
// tags into one file for each specific subtag
|
||||
//
|
||||
// TODO: This is a naive interpretation of BCP47 that should be improved.
|
||||
// Will be OK for now as long as we don't have anything more complicated
|
||||
// than a base translation + region.
|
||||
const tr = path.basename(file.history[0], ".json");
|
||||
const subtags = tr.split("-");
|
||||
const src = [workDir + "/translationMaster.json"];
|
||||
for (let i = 1; i <= subtags.length; i++) {
|
||||
const lang = subtags.slice(0, i).join("-");
|
||||
if (lang === "test") {
|
||||
src.push(workDir + "/test.json");
|
||||
} else if (lang !== "en") {
|
||||
src.push(inDir + "/" + lang + ".json");
|
||||
}
|
||||
gulp.task("build-merged-translations", function() {
|
||||
return gulp
|
||||
.src([inDir + "/*.json", workDir + "/test.json"], { allowEmpty: true })
|
||||
.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
|
||||
// translation as a failsafe for untranslated strings, and merges all parent
|
||||
// tags into one file for each specific subtag
|
||||
//
|
||||
// TODO: This is a naive interpretation of BCP47 that should be improved.
|
||||
// Will be OK for now as long as we don't have anything more complicated
|
||||
// than a base translation + region.
|
||||
const tr = path.basename(file.history[0], ".json");
|
||||
const subtags = tr.split("-");
|
||||
const src = [workDir + "/translationMaster.json"];
|
||||
for (let i = 1; i <= subtags.length; i++) {
|
||||
const lang = subtags.slice(0, i).join("-");
|
||||
if (lang === "test") {
|
||||
src.push(workDir + "/test.json");
|
||||
} else if (lang !== "en") {
|
||||
src.push(inDir + "/" + lang + ".json");
|
||||
}
|
||||
return gulp
|
||||
.src(src, { allowEmpty: true })
|
||||
.pipe(transform((data) => emptyFilter(data)))
|
||||
.pipe(
|
||||
merge({
|
||||
fileName: tr + ".json",
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(fullDir));
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
}
|
||||
return gulp
|
||||
.src(src, { allowEmpty: true })
|
||||
.pipe(transform((data) => emptyFilter(data)))
|
||||
.pipe(
|
||||
merge({
|
||||
fileName: tr + ".json",
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(fullDir));
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
var taskName;
|
||||
|
||||
const splitTasks = [];
|
||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||
taskName = "build-translation-fragment-" + fragment;
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("build-merged-translations", function() {
|
||||
// Return only the translations for this fragment.
|
||||
return gulp
|
||||
.src(fullDir + "/*.json")
|
||||
.pipe(
|
||||
transform((data) => ({
|
||||
ui: {
|
||||
panel: {
|
||||
[fragment]: data.ui.panel[fragment],
|
||||
},
|
||||
gulp.task(taskName, function() {
|
||||
// Return only the translations for this fragment.
|
||||
return gulp
|
||||
.src(fullDir + "/*.json")
|
||||
.pipe(
|
||||
transform((data) => ({
|
||||
ui: {
|
||||
panel: {
|
||||
[fragment]: data.ui.panel[fragment],
|
||||
},
|
||||
}))
|
||||
)
|
||||
.pipe(gulp.dest(workDir + "/" + fragment));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
},
|
||||
}))
|
||||
)
|
||||
.pipe(gulp.dest(workDir + "/" + fragment));
|
||||
});
|
||||
splitTasks.push(taskName);
|
||||
});
|
||||
|
||||
taskName = "build-translation-core";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("build-merged-translations", function() {
|
||||
// Remove the fragment translations from the core translation.
|
||||
return gulp
|
||||
.src(fullDir + "/*.json")
|
||||
.pipe(
|
||||
transform((data) => {
|
||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||
delete data.ui.panel[fragment];
|
||||
});
|
||||
return data;
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(coreDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
gulp.task(taskName, function() {
|
||||
// Remove the fragment translations from the core translation.
|
||||
return gulp
|
||||
.src(fullDir + "/*.json")
|
||||
.pipe(
|
||||
transform((data) => {
|
||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||
delete data.ui.panel[fragment];
|
||||
});
|
||||
return data;
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(coreDir));
|
||||
});
|
||||
|
||||
splitTasks.push(taskName);
|
||||
|
||||
taskName = "build-flattened-translations";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series(...splitTasks, function() {
|
||||
// Flatten the split versions of our translations, and move them into outDir
|
||||
return gulp
|
||||
.src(
|
||||
TRANSLATION_FRAGMENTS.map(
|
||||
(fragment) => workDir + "/" + fragment + "/*.json"
|
||||
).concat(coreDir + "/*.json"),
|
||||
{ base: workDir }
|
||||
)
|
||||
.pipe(
|
||||
transform(function(data) {
|
||||
// Polymer.AppLocalizeBehavior requires flattened json
|
||||
return flatten(data);
|
||||
})
|
||||
)
|
||||
.pipe(minify())
|
||||
.pipe(hashFilename())
|
||||
.pipe(
|
||||
rename((filePath) => {
|
||||
if (filePath.dirname === "core") {
|
||||
filePath.dirname = "";
|
||||
}
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(outDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
gulp.task("build-flattened-translations", function() {
|
||||
// Flatten the split versions of our translations, and move them into outDir
|
||||
return gulp
|
||||
.src(
|
||||
TRANSLATION_FRAGMENTS.map(
|
||||
(fragment) => workDir + "/" + fragment + "/*.json"
|
||||
).concat(coreDir + "/*.json"),
|
||||
{ base: workDir }
|
||||
)
|
||||
.pipe(
|
||||
transform(function(data) {
|
||||
// Polymer.AppLocalizeBehavior requires flattened json
|
||||
return flatten(data);
|
||||
})
|
||||
)
|
||||
.pipe(minify())
|
||||
.pipe(
|
||||
rename((filePath) => {
|
||||
if (filePath.dirname === "core") {
|
||||
filePath.dirname = "";
|
||||
}
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(outDir));
|
||||
});
|
||||
|
||||
taskName = "build-translation-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);
|
||||
const fingerprints = {};
|
||||
|
||||
taskName = "build-translations";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("build-translation-fingerprints", function() {
|
||||
return gulp
|
||||
.src(
|
||||
[
|
||||
"src/translations/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));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
"build-translation-fingerprints",
|
||||
function fingerprintTranslationFiles() {
|
||||
// Fingerprint full file of each language
|
||||
const files = fs.readdirSync(fullDir);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
fingerprints[files[i].split(".")[0]] = {
|
||||
// In dev we create fake hashes
|
||||
hash: env.isProdBuild
|
||||
? crypto
|
||||
.createHash("md5")
|
||||
.update(fs.readFileSync(path.join(fullDir, files[i]), "utf-8"))
|
||||
.digest("hex")
|
||||
: "dev",
|
||||
};
|
||||
}
|
||||
|
||||
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 WebpackDevServer = require("webpack-dev-server");
|
||||
const log = require("fancy-log");
|
||||
const path = require("path");
|
||||
const paths = require("../paths");
|
||||
const {
|
||||
createAppConfig,
|
||||
@@ -57,10 +58,14 @@ const handler = (done) => (err, stats) => {
|
||||
|
||||
gulp.task("webpack-watch-app", () => {
|
||||
// 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()
|
||||
);
|
||||
gulp.watch(
|
||||
path.join(paths.translations_src, "en.json"),
|
||||
gulp.series("build-translations", "copy-translations")
|
||||
);
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
|
@@ -29,4 +29,6 @@ module.exports = {
|
||||
hassio_dir: path.resolve(__dirname, "../hassio"),
|
||||
hassio_root: path.resolve(__dirname, "../hassio/build"),
|
||||
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
|
||||
const translationMetadata = require("../build-translations/translationMetadata.json");
|
||||
const workBoxTranslationsTemplatedURLs = {};
|
||||
const englishFP = translationMetadata.translations.en.fingerprints;
|
||||
Object.keys(englishFP).forEach((key) => {
|
||||
const englishFilename = `en-${translationMetadata.translations.en.hash}.json`;
|
||||
|
||||
// core
|
||||
workBoxTranslationsTemplatedURLs[
|
||||
`/static/translations/${englishFilename}`
|
||||
] = `build-translations/output/${englishFilename}`;
|
||||
|
||||
Object.keys(translationMetadata.fragments).forEach((fragment) => {
|
||||
workBoxTranslationsTemplatedURLs[
|
||||
`/static/translations/${englishFP[key]}`
|
||||
] = `build-translations/output/${key}.json`;
|
||||
`/static/translations/${fragment}/${englishFilename}`
|
||||
] = `build-translations/output/${fragment}/${englishFilename}`;
|
||||
});
|
||||
|
||||
config.plugins.push(
|
||||
|
@@ -26,10 +26,12 @@ import { CastManager } from "../../../../src/cast/cast_manager";
|
||||
import {
|
||||
LovelaceConfig,
|
||||
getLovelaceCollection,
|
||||
getLegacyLovelaceCollection,
|
||||
} from "../../../../src/data/lovelace";
|
||||
import "./hc-layout";
|
||||
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
|
||||
import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute";
|
||||
import { atLeastVersion } from "../../../../src/common/config/version";
|
||||
|
||||
@customElement("hc-cast")
|
||||
class HcCast extends LitElement {
|
||||
@@ -133,7 +135,9 @@ class HcCast extends LitElement {
|
||||
protected 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
|
||||
// configuration.
|
||||
llColl.refresh().then(
|
||||
|
@@ -15,6 +15,9 @@ import {
|
||||
import {
|
||||
LovelaceConfig,
|
||||
getLovelaceCollection,
|
||||
fetchResources,
|
||||
LegacyLovelaceConfig,
|
||||
getLegacyLovelaceCollection,
|
||||
} from "../../../../src/data/lovelace";
|
||||
import "./hc-launch-screen";
|
||||
import { castContext } from "../cast_context";
|
||||
@@ -22,6 +25,9 @@ import { CAST_NS } from "../../../../src/cast/const";
|
||||
import { ReceiverStatusMessage } from "../../../../src/cast/sender_messages";
|
||||
import { loadLovelaceResources } from "../../../../src/panels/lovelace/common/load-resources";
|
||||
import { isNavigationClick } from "../../../../src/common/dom/is-navigation-click";
|
||||
import { atLeastVersion } from "../../../../src/common/config/version";
|
||||
|
||||
let resourcesLoaded = false;
|
||||
|
||||
@customElement("hc-main")
|
||||
export class HcMain extends HassElement {
|
||||
@@ -34,6 +40,7 @@ export class HcMain extends HassElement {
|
||||
@property() private _error?: string;
|
||||
|
||||
private _unsubLovelace?: UnsubscribeFunc;
|
||||
private _urlPath?: string | null;
|
||||
|
||||
public processIncomingMessage(msg: HassMessage) {
|
||||
if (msg.type === "connect") {
|
||||
@@ -108,6 +115,7 @@ export class HcMain extends HassElement {
|
||||
if (this.hass) {
|
||||
status.hassUrl = this.hass.auth.data.hassUrl;
|
||||
status.lovelacePath = this._lovelacePath!;
|
||||
status.urlPath = this._urlPath;
|
||||
}
|
||||
|
||||
if (senderId) {
|
||||
@@ -163,8 +171,14 @@ export class HcMain extends HassElement {
|
||||
this._error = "Cannot show Lovelace because we're not connected.";
|
||||
return;
|
||||
}
|
||||
if (!this._unsubLovelace) {
|
||||
const llColl = getLovelaceCollection(this.hass!.connection);
|
||||
if (!this._unsubLovelace || this._urlPath !== msg.urlPath) {
|
||||
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
|
||||
// configuration.
|
||||
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._lovelacePath = msg.viewPath;
|
||||
if (castContext.getDeviceCapabilities().touch_input_supported) {
|
||||
@@ -194,12 +217,6 @@ export class HcMain extends HassElement {
|
||||
private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) {
|
||||
castContext.setApplicationState(lovelaceConfig.title!);
|
||||
this._lovelaceConfig = lovelaceConfig;
|
||||
if (lovelaceConfig.resources) {
|
||||
loadLovelaceResources(
|
||||
lovelaceConfig.resources,
|
||||
this.hass!.auth.data.hassUrl
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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: 30 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: [
|
||||
{
|
||||
entity: "script.air_cleaner_quiet",
|
||||
type: "entity-button",
|
||||
type: "button",
|
||||
name: "AC bed",
|
||||
tap_action: {
|
||||
action: "call-service",
|
||||
@@ -408,7 +408,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
},
|
||||
{
|
||||
entity: "script.air_cleaner_auto",
|
||||
type: "entity-button",
|
||||
type: "button",
|
||||
name: "AC bed",
|
||||
tap_action: {
|
||||
action: "call-service",
|
||||
@@ -421,7 +421,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
},
|
||||
{
|
||||
entity: "script.air_cleaner_turbo",
|
||||
type: "entity-button",
|
||||
type: "button",
|
||||
name: "AC bed",
|
||||
tap_action: {
|
||||
action: "call-service",
|
||||
@@ -434,7 +434,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
},
|
||||
{
|
||||
entity: "script.ac_off",
|
||||
type: "entity-button",
|
||||
type: "button",
|
||||
name: "AC",
|
||||
tap_action: {
|
||||
action: "call-service",
|
||||
@@ -447,7 +447,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
},
|
||||
{
|
||||
entity: "script.ac_on",
|
||||
type: "entity-button",
|
||||
type: "button",
|
||||
name: "AC",
|
||||
tap_action: {
|
||||
action: "call-service",
|
||||
@@ -658,7 +658,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
action: "call-service",
|
||||
service: "script.goodnight",
|
||||
},
|
||||
type: "entity-button",
|
||||
type: "button",
|
||||
icon: "mdi:weather-night",
|
||||
},
|
||||
{
|
||||
@@ -670,7 +670,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
},
|
||||
service: "scene.turn_on",
|
||||
},
|
||||
type: "entity-button",
|
||||
type: "button",
|
||||
icon: "mdi:coffee-outline",
|
||||
},
|
||||
{
|
||||
@@ -682,7 +682,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
},
|
||||
service: "scene.turn_on",
|
||||
},
|
||||
type: "entity-button",
|
||||
type: "button",
|
||||
icon: "mdi:television-classic",
|
||||
},
|
||||
],
|
||||
@@ -743,7 +743,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
},
|
||||
service: "light.toggle",
|
||||
},
|
||||
type: "entity-button",
|
||||
type: "button",
|
||||
icon: "mdi:page-layout-footer",
|
||||
},
|
||||
{
|
||||
@@ -755,7 +755,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
},
|
||||
service: "light.toggle",
|
||||
},
|
||||
type: "entity-button",
|
||||
type: "button",
|
||||
icon: "mdi:page-layout-header",
|
||||
},
|
||||
],
|
||||
|
BIN
gallery/public/images/album_cover.jpg
Normal file
BIN
gallery/public/images/album_cover.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
62
gallery/src/data/media_players.ts
Normal file
62
gallery/src/data/media_players.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
|
||||
export const createMediaPlayerEntities = () => [
|
||||
getEntity("media_player", "bedroom", "playing", {
|
||||
media_content_type: "movie",
|
||||
media_title: "Epic sax guy 10 hours",
|
||||
app_name: "YouTube",
|
||||
friendly_name: "Skip, no pause",
|
||||
supported_features: 32,
|
||||
}),
|
||||
getEntity("media_player", "family_room", "paused", {
|
||||
friendly_name: "Paused, music",
|
||||
media_content_type: "music",
|
||||
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
|
||||
media_artist: "Technohead",
|
||||
supported_features: 16417,
|
||||
entity_picture: "/images/album_cover.jpg",
|
||||
}),
|
||||
getEntity("media_player", "family_room_no_play", "paused", {
|
||||
friendly_name: "Paused, no play",
|
||||
media_content_type: "movie",
|
||||
media_title: "Epic sax guy 10 hours",
|
||||
app_name: "YouTube",
|
||||
supported_features: 33,
|
||||
}),
|
||||
getEntity("media_player", "living_room", "playing", {
|
||||
friendly_name: "Pause, No skip, tvshow",
|
||||
media_content_type: "tvshow",
|
||||
media_title: "Chapter 1",
|
||||
media_series_title: "House of Cards",
|
||||
app_name: "Netflix",
|
||||
supported_features: 1,
|
||||
}),
|
||||
getEntity("media_player", "lounge_room", "idle", {
|
||||
friendly_name: "Screen casting",
|
||||
media_content_type: "music",
|
||||
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
|
||||
media_artist: "Technohead",
|
||||
supported_features: 1,
|
||||
}),
|
||||
getEntity("media_player", "theater", "off", {
|
||||
friendly_name: "Chromcast Idle",
|
||||
media_content_type: "movie",
|
||||
media_title: "Epic sax guy 10 hours",
|
||||
app_name: "YouTube",
|
||||
supported_features: 33,
|
||||
}),
|
||||
getEntity("media_player", "android_cast", "playing", {
|
||||
friendly_name: "Player Off",
|
||||
media_title: "Android Screen Casting",
|
||||
app_name: "Screen Mirroring",
|
||||
supported_features: 21437,
|
||||
}),
|
||||
getEntity("media_player", "unavailable", "unavailable", {
|
||||
friendly_name: "Player Unavailable",
|
||||
supported_features: 21437,
|
||||
}),
|
||||
getEntity("media_player", "unknown", "unknown", {
|
||||
friendly_name: "Player Unknown",
|
||||
supported_features: 21437,
|
||||
}),
|
||||
];
|
@@ -15,14 +15,14 @@ const CONFIGS = [
|
||||
{
|
||||
heading: "Basic example",
|
||||
config: `
|
||||
- type: entity-button
|
||||
- type: button
|
||||
entity: light.bed_light
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "With Name",
|
||||
config: `
|
||||
- type: entity-button
|
||||
- type: button
|
||||
name: Bedroom
|
||||
entity: light.bed_light
|
||||
`,
|
||||
@@ -30,7 +30,7 @@ const CONFIGS = [
|
||||
{
|
||||
heading: "With Icon",
|
||||
config: `
|
||||
- type: entity-button
|
||||
- type: button
|
||||
entity: light.bed_light
|
||||
icon: mdi:hotel
|
||||
`,
|
||||
@@ -38,7 +38,7 @@ const CONFIGS = [
|
||||
{
|
||||
heading: "Without State",
|
||||
config: `
|
||||
- type: entity-button
|
||||
- type: button
|
||||
entity: light.bed_light
|
||||
show_state: false
|
||||
`,
|
||||
@@ -46,7 +46,7 @@ const CONFIGS = [
|
||||
{
|
||||
heading: "Custom Tap Action (toggle)",
|
||||
config: `
|
||||
- type: entity-button
|
||||
- type: button
|
||||
entity: light.bed_light
|
||||
tap_action:
|
||||
action: toggle
|
||||
@@ -55,7 +55,7 @@ const CONFIGS = [
|
||||
{
|
||||
heading: "Running Service",
|
||||
config: `
|
||||
- type: entity-button
|
||||
- type: button
|
||||
entity: light.bed_light
|
||||
service: light.toggle
|
||||
`,
|
||||
@@ -63,13 +63,13 @@ const CONFIGS = [
|
||||
{
|
||||
heading: "Invalid Entity",
|
||||
config: `
|
||||
- type: entity-button
|
||||
- type: button
|
||||
entity: sensor.invalid_entity
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
class DemoEntityButtonEntity extends PolymerElement {
|
||||
class DemoButtonEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<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);
|
||||
|
102
gallery/src/demos/demo-hui-media-control-card.ts
Normal file
102
gallery/src/demos/demo-hui-media-control-card.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
import { createMediaPlayerEntities } from "../data/media_players";
|
||||
|
||||
const CONFIGS = [
|
||||
{
|
||||
heading: "Skip, no pause",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.bedroom
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Paused, music",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.family_room
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Paused, no play",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.family_room_no_play
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Pause, No skip, tvshow",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.living_room
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Screen casting",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.android_cast
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Chromcast Idle",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.lounge_room
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Player Off",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.theater
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Player Unavailable",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.unavailable
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Player Unknown",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.unknown
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
class DemoHuiMediControlCard extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards
|
||||
id="demos"
|
||||
hass="[[hass]]"
|
||||
configs="[[_configs]]"
|
||||
></demo-cards>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
hass: Object,
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(createMediaPlayerEntities());
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-media-control-card", DemoHuiMediControlCard);
|
@@ -1,54 +1,9 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("media_player", "bedroom", "playing", {
|
||||
media_content_type: "movie",
|
||||
media_title: "Epic sax guy 10 hours",
|
||||
app_name: "YouTube",
|
||||
supported_features: 32,
|
||||
}),
|
||||
getEntity("media_player", "family_room", "paused", {
|
||||
media_content_type: "music",
|
||||
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
|
||||
media_artist: "Technohead",
|
||||
supported_features: 16417,
|
||||
}),
|
||||
getEntity("media_player", "family_room_no_play", "paused", {
|
||||
media_content_type: "movie",
|
||||
media_title: "Epic sax guy 10 hours",
|
||||
app_name: "YouTube",
|
||||
supported_features: 33,
|
||||
}),
|
||||
getEntity("media_player", "living_room", "playing", {
|
||||
media_content_type: "tvshow",
|
||||
media_title: "Chapter 1",
|
||||
media_series_title: "House of Cards",
|
||||
app_name: "Netflix",
|
||||
supported_features: 1,
|
||||
}),
|
||||
getEntity("media_player", "lounge_room", "idle", {
|
||||
media_content_type: "music",
|
||||
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
|
||||
media_artist: "Technohead",
|
||||
supported_features: 1,
|
||||
}),
|
||||
getEntity("media_player", "theater", "off", {
|
||||
media_content_type: "movie",
|
||||
media_title: "Epic sax guy 10 hours",
|
||||
app_name: "YouTube",
|
||||
supported_features: 33,
|
||||
}),
|
||||
getEntity("media_player", "android_cast", "playing", {
|
||||
media_title: "Android Screen Casting",
|
||||
app_name: "Screen Mirroring",
|
||||
supported_features: 21437,
|
||||
}),
|
||||
];
|
||||
import { createMediaPlayerEntities } from "../data/media_players";
|
||||
|
||||
const CONFIGS = [
|
||||
{
|
||||
@@ -69,7 +24,11 @@ const CONFIGS = [
|
||||
- entity: media_player.lounge_room
|
||||
name: Chromcast Idle
|
||||
- entity: media_player.theater
|
||||
name: 'Player Off'
|
||||
name: Player Off
|
||||
- entity: media_player.unavailable
|
||||
name: Player Unavailable
|
||||
- entity: media_player.unknown
|
||||
name: Player Unknown
|
||||
`,
|
||||
},
|
||||
];
|
||||
@@ -98,7 +57,7 @@ class DemoHuiMediaPlayerRows extends PolymerElement {
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
||||
hass.addEntities(createMediaPlayerEntities());
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -18,6 +18,7 @@ import {
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { filterAndSort } from "../components/hassio-filter-addons";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
|
||||
class HassioAddonRepositoryEl extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@@ -42,75 +43,81 @@ class HassioAddonRepositoryEl extends LitElement {
|
||||
|
||||
if (this.filter && addons.length < 1) {
|
||||
return html`
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
<div class="description">
|
||||
No results found in "${repo.name}"
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p class="description">
|
||||
No results found in "${repo.name}"
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
<div class="content">
|
||||
<h1>
|
||||
${repo.name}
|
||||
<div class="description">
|
||||
Maintained by ${repo.maintainer}<br />
|
||||
<a class="repo" href=${repo.url} target="_blank">${repo.url}</a>
|
||||
</div>
|
||||
</h1>
|
||||
<p class="description">
|
||||
Maintained by ${repo.maintainer}<br />
|
||||
<a class="repo" href=${repo.url} target="_blank" rel="noreferrer">
|
||||
${repo.url}
|
||||
</a>
|
||||
</p>
|
||||
<div class="card-group">
|
||||
${addons.map(
|
||||
(addon) => html`
|
||||
<paper-card
|
||||
.addon=${addon}
|
||||
class=${addon.available ? "" : "not_available"}
|
||||
@click=${this._addonTapped}
|
||||
>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${addon.name}
|
||||
.description=${addon.description}
|
||||
.available=${addon.available}
|
||||
.icon=${addon.installed && addon.installed !== addon.version
|
||||
? "hassio:arrow-up-bold-circle"
|
||||
: "hassio:puzzle"}
|
||||
.iconTitle=${addon.installed
|
||||
? addon.installed !== addon.version
|
||||
? "New version available"
|
||||
: "Add-on is installed"
|
||||
: addon.available
|
||||
? "Add-on is not installed"
|
||||
: "Add-on is not available on your system"}
|
||||
.iconClass=${addon.installed
|
||||
? addon.installed !== addon.version
|
||||
? "update"
|
||||
: "installed"
|
||||
: !addon.available
|
||||
? "not_available"
|
||||
: ""}
|
||||
.iconImage=${atLeastVersion(
|
||||
this.hass.connection.haVersion,
|
||||
0,
|
||||
105
|
||||
) && addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined}
|
||||
.showTopbar=${addon.installed || !addon.available}
|
||||
.topbarClass=${addon.installed
|
||||
? addon.installed !== addon.version
|
||||
? "update"
|
||||
: "installed"
|
||||
: !addon.available
|
||||
? "unavailable"
|
||||
: ""}
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
</paper-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
|
||||
${addons.map(
|
||||
(addon) => html`
|
||||
<paper-card
|
||||
.addon=${addon}
|
||||
class=${addon.available ? "" : "not_available"}
|
||||
@click=${this.addonTapped}
|
||||
>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${addon.name}
|
||||
.description=${addon.description}
|
||||
.available=${addon.available}
|
||||
.icon=${this.computeIcon(addon)}
|
||||
.iconTitle=${this.computeIconTitle(addon)}
|
||||
.iconClass=${this.computeIconClass(addon)}
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
</paper-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private computeIcon(addon) {
|
||||
return addon.installed && addon.installed !== addon.version
|
||||
? "hassio:arrow-up-bold-circle"
|
||||
: "hassio:puzzle";
|
||||
}
|
||||
|
||||
private computeIconTitle(addon) {
|
||||
if (addon.installed) {
|
||||
return addon.installed !== addon.version
|
||||
? "New version available"
|
||||
: "Add-on is installed";
|
||||
}
|
||||
return addon.available
|
||||
? "Add-on is not installed"
|
||||
: "Add-on is not available on your system";
|
||||
}
|
||||
|
||||
private computeIconClass(addon) {
|
||||
if (addon.installed) {
|
||||
return addon.installed !== addon.version ? "update" : "installed";
|
||||
}
|
||||
return !addon.available ? "not_available" : "";
|
||||
}
|
||||
|
||||
private addonTapped(ev) {
|
||||
private _addonTapped(ev) {
|
||||
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`);
|
||||
}
|
||||
|
||||
|
@@ -36,61 +36,63 @@ class HassioRepositoriesEditor extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
const repos = this._sortedRepos(this.repos);
|
||||
return html`
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
<div class="content">
|
||||
<h1>
|
||||
Repositories
|
||||
<div class="description">
|
||||
Configure which add-on repositories to fetch data from:
|
||||
</div>
|
||||
</div>
|
||||
${// Use repeat so that the fade-out from call-service-api-button
|
||||
// stays with the correct repo after we add/delete one.
|
||||
repeat(
|
||||
repos,
|
||||
(repo) => repo.slug,
|
||||
(repo) => html`
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${repo.name}
|
||||
.description=${repo.url}
|
||||
icon="hassio:github-circle"
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button
|
||||
path="hassio/supervisor/options"
|
||||
.hass=${this.hass}
|
||||
.data=${this.computeRemoveRepoData(repos, repo.url)}
|
||||
class="warning"
|
||||
>
|
||||
Remove
|
||||
</ha-call-api-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`
|
||||
)}
|
||||
</h1>
|
||||
<p class="description">
|
||||
Configure which add-on repositories to fetch data from:
|
||||
</p>
|
||||
<div class="card-group">
|
||||
${// Use repeat so that the fade-out from call-service-api-button
|
||||
// stays with the correct repo after we add/delete one.
|
||||
repeat(
|
||||
repos,
|
||||
(repo) => repo.slug,
|
||||
(repo) => html`
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${repo.name}
|
||||
.description=${repo.url}
|
||||
icon="hassio:github-circle"
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button
|
||||
path="hassio/supervisor/options"
|
||||
.hass=${this.hass}
|
||||
.data=${this.computeRemoveRepoData(repos, repo.url)}
|
||||
class="warning"
|
||||
>
|
||||
Remove
|
||||
</ha-call-api-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`
|
||||
)}
|
||||
|
||||
<paper-card>
|
||||
<div class="card-content add">
|
||||
<iron-icon icon="hassio:github-circle"></iron-icon>
|
||||
<paper-input
|
||||
label="Add new repository by URL"
|
||||
.value=${this._repoUrl}
|
||||
@value-changed=${this._urlChanged}
|
||||
></paper-input>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button
|
||||
path="hassio/supervisor/options"
|
||||
.hass=${this.hass}
|
||||
.data=${this.computeAddRepoData(repos, this._repoUrl)}
|
||||
>
|
||||
Add
|
||||
</ha-call-api-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
<paper-card>
|
||||
<div class="card-content add">
|
||||
<iron-icon icon="hassio:github-circle"></iron-icon>
|
||||
<paper-input
|
||||
label="Add new repository by URL"
|
||||
.value=${this._repoUrl}
|
||||
@value-changed=${this._urlChanged}
|
||||
></paper-input>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button
|
||||
path="hassio/supervisor/options"
|
||||
.hass=${this.hass}
|
||||
.data=${this.computeAddRepoData(repos, this._repoUrl)}
|
||||
>
|
||||
Add
|
||||
</ha-call-api-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
@@ -51,7 +51,7 @@ class HassioAddonAudio extends LitElement {
|
||||
|
||||
<paper-dropdown-menu
|
||||
label="Input"
|
||||
@selected-item-changed=${this._setInputDevice}
|
||||
@iron-select=${this._setInputDevice}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
@@ -61,14 +61,16 @@ class HassioAddonAudio extends LitElement {
|
||||
${this._inputDevices &&
|
||||
this._inputDevices.map((item) => {
|
||||
return html`
|
||||
<paper-item device=${item.device}>${item.name}</paper-item>
|
||||
<paper-item device=${item.device || ""}
|
||||
>${item.name}</paper-item
|
||||
>
|
||||
`;
|
||||
})}
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
<paper-dropdown-menu
|
||||
label="Output"
|
||||
@selected-item-changed=${this._setOutputDevice}
|
||||
@iron-select=${this._setOutputDevice}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
@@ -78,7 +80,9 @@ class HassioAddonAudio extends LitElement {
|
||||
${this._outputDevices &&
|
||||
this._outputDevices.map((item) => {
|
||||
return html`
|
||||
<paper-item device=${item.device}>${item.name}</paper-item>
|
||||
<paper-item device=${item.device || ""}
|
||||
>${item.name}</paper-item
|
||||
>
|
||||
`;
|
||||
})}
|
||||
</paper-listbox>
|
||||
@@ -123,33 +127,32 @@ class HassioAddonAudio extends LitElement {
|
||||
}
|
||||
|
||||
private _setInputDevice(ev): void {
|
||||
const device = ev.detail.device;
|
||||
if (device) {
|
||||
this._selectedInput = device;
|
||||
}
|
||||
const device = ev.detail.item.getAttribute("device");
|
||||
this._selectedInput = device;
|
||||
}
|
||||
|
||||
private _setOutputDevice(ev): void {
|
||||
const device = ev.detail.device;
|
||||
if (device) {
|
||||
this._selectedOutput = device;
|
||||
}
|
||||
const device = ev.detail.item.getAttribute("device");
|
||||
this._selectedOutput = device;
|
||||
}
|
||||
|
||||
private async _addonChanged(): Promise<void> {
|
||||
this._selectedInput = this.addon.audio_input;
|
||||
this._selectedOutput = this.addon.audio_output;
|
||||
this._selectedInput =
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
const noDevice: HassioHardwareAudioDevice[] = [
|
||||
{ device: undefined, name: "-" },
|
||||
];
|
||||
const noDevice: HassioHardwareAudioDevice = {
|
||||
device: "default",
|
||||
name: "Default",
|
||||
};
|
||||
|
||||
try {
|
||||
const { audio } = await fetchHassioHardwareAudio(this.hass);
|
||||
const inupt = Object.keys(audio.input).map((key) => ({
|
||||
const input = Object.keys(audio.input).map((key) => ({
|
||||
device: key,
|
||||
name: audio.input[key],
|
||||
}));
|
||||
@@ -158,20 +161,22 @@ class HassioAddonAudio extends LitElement {
|
||||
name: audio.output[key],
|
||||
}));
|
||||
|
||||
this._inputDevices = noDevice.concat(inupt);
|
||||
this._outputDevices = noDevice.concat(output);
|
||||
this._inputDevices = [noDevice, ...input];
|
||||
this._outputDevices = [noDevice, ...output];
|
||||
} catch {
|
||||
this._error = "Failed to fetch audio hardware";
|
||||
this._inputDevices = noDevice;
|
||||
this._outputDevices = noDevice;
|
||||
this._inputDevices = [noDevice];
|
||||
this._outputDevices = [noDevice];
|
||||
}
|
||||
}
|
||||
|
||||
private async _saveSettings(): Promise<void> {
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
audio_input: this._selectedInput || null,
|
||||
audio_output: this._selectedOutput || null,
|
||||
audio_input:
|
||||
this._selectedInput === "default" ? null : this._selectedInput,
|
||||
audio_output:
|
||||
this._selectedOutput === "default" ? null : this._selectedOutput,
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
|
@@ -10,6 +10,7 @@ import {
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
query,
|
||||
} from "lit-element";
|
||||
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
@@ -20,30 +21,42 @@ import {
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { PolymerChangedEvent } from "../../../src/polymer-types";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/ha-yaml-editor";
|
||||
// tslint:disable-next-line: no-duplicate-imports
|
||||
import { HaYamlEditor } from "../../../src/components/ha-yaml-editor";
|
||||
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
|
||||
@customElement("hassio-addon-config")
|
||||
class HassioAddonConfig extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public addon!: HassioAddonDetails;
|
||||
@property() private _error?: string;
|
||||
@property() private _config!: string;
|
||||
@property({ type: Boolean }) private _configHasChanged = false;
|
||||
|
||||
@query("ha-yaml-editor") private _editor!: HaYamlEditor;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const editor = this._editor;
|
||||
// If editor not rendered, don't show the error.
|
||||
const valid = editor ? editor.isValid : true;
|
||||
|
||||
return html`
|
||||
<paper-card heading="Config">
|
||||
<div class="card-content">
|
||||
<ha-yaml-editor
|
||||
@value-changed=${this._configChanged}
|
||||
></ha-yaml-editor>
|
||||
${this._error
|
||||
? html`
|
||||
<div class="errors">${this._error}</div>
|
||||
`
|
||||
: ""}
|
||||
<iron-autogrow-textarea
|
||||
@value-changed=${this._configChanged}
|
||||
.value=${this._config}
|
||||
></iron-autogrow-textarea>
|
||||
${valid
|
||||
? ""
|
||||
: html`
|
||||
<div class="errors">Invalid YAML</div>
|
||||
`}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button class="warning" @click=${this._resetTapped}>
|
||||
@@ -51,7 +64,7 @@ class HassioAddonConfig extends LitElement {
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
@click=${this._saveTapped}
|
||||
.disabled=${!this._configHasChanged}
|
||||
.disabled=${!this._configHasChanged || !valid}
|
||||
>
|
||||
Save
|
||||
</mwc-button>
|
||||
@@ -77,7 +90,7 @@ class HassioAddonConfig extends LitElement {
|
||||
}
|
||||
.errors {
|
||||
color: var(--google-red-500);
|
||||
margin-bottom: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
iron-autogrow-textarea {
|
||||
width: 100%;
|
||||
@@ -93,18 +106,26 @@ class HassioAddonConfig extends LitElement {
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("addon")) {
|
||||
this._config = JSON.stringify(this.addon.options, null, 2);
|
||||
this._editor.setValue(this.addon.options);
|
||||
}
|
||||
}
|
||||
|
||||
private _configChanged(ev: PolymerChangedEvent<string>): void {
|
||||
this._config =
|
||||
ev.detail.value || JSON.stringify(this.addon.options, null, 2);
|
||||
this._configHasChanged =
|
||||
this._config !== JSON.stringify(this.addon.options, null, 2);
|
||||
private _configChanged(): void {
|
||||
this._configHasChanged = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private async _resetTapped(): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.addon.name,
|
||||
text: "Are you sure you want to reset all your options?",
|
||||
confirmText: "reset options",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
options: null,
|
||||
@@ -129,7 +150,7 @@ class HassioAddonConfig extends LitElement {
|
||||
this._error = undefined;
|
||||
try {
|
||||
data = {
|
||||
options: JSON.parse(this._config),
|
||||
options: this._editor.value,
|
||||
};
|
||||
} catch (err) {
|
||||
this._error = err;
|
||||
|
@@ -36,6 +36,7 @@ import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
|
||||
const PERMIS_DESC = {
|
||||
rating: {
|
||||
@@ -185,14 +186,19 @@ class HassioAddonInfo extends LitElement {
|
||||
<div class="description light-color">
|
||||
${this.addon.description}.<br />
|
||||
Visit
|
||||
<a href="${this.addon.url}" target="_blank">
|
||||
<a href="${this.addon.url}" target="_blank" rel="noreferrer">
|
||||
${this.addon.name} page</a
|
||||
>
|
||||
for details.
|
||||
</div>
|
||||
${this.addon.logo
|
||||
? 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" />
|
||||
</a>
|
||||
`
|
||||
@@ -319,6 +325,7 @@ class HassioAddonInfo extends LitElement {
|
||||
<ha-switch
|
||||
@change=${this._startOnBootToggled}
|
||||
.checked=${this.addon.boot === "auto"}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</div>
|
||||
<div class="state">
|
||||
@@ -326,6 +333,7 @@ class HassioAddonInfo extends LitElement {
|
||||
<ha-switch
|
||||
@change=${this._autoUpdateToggled}
|
||||
.checked=${this.addon.auto_update}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</div>
|
||||
${this.addon.ingress
|
||||
@@ -336,6 +344,7 @@ class HassioAddonInfo extends LitElement {
|
||||
@change=${this._panelToggled}
|
||||
.checked=${this.addon.ingress_panel}
|
||||
.disabled=${this._computeCannotIngressSidebar}
|
||||
haptic
|
||||
></ha-switch>
|
||||
${this._computeCannotIngressSidebar
|
||||
? html`
|
||||
@@ -363,6 +372,7 @@ class HassioAddonInfo extends LitElement {
|
||||
<ha-switch
|
||||
@change=${this._protectionToggled}
|
||||
.checked=${this.addon.protected}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</div>
|
||||
`
|
||||
@@ -424,6 +434,7 @@ class HassioAddonInfo extends LitElement {
|
||||
tabindex="-1"
|
||||
target="_blank"
|
||||
class="right"
|
||||
rel="noopener"
|
||||
>
|
||||
<mwc-button>
|
||||
Open web UI
|
||||
@@ -448,9 +459,8 @@ class HassioAddonInfo extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
<ha-progress-button
|
||||
.disabled=${!this.addon.available}
|
||||
.disabled=${!this.addon.available || this._installing}
|
||||
.progress=${this._installing}
|
||||
class="right"
|
||||
@click=${this._installClicked}
|
||||
>
|
||||
Install
|
||||
@@ -650,7 +660,10 @@ class HassioAddonInfo extends LitElement {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -659,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> {
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
|
@@ -17,21 +17,40 @@ class HassioCardContent extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public title!: string;
|
||||
@property() public description?: string;
|
||||
@property({ type: Boolean }) public available?: boolean;
|
||||
@property({ type: Boolean }) public available: boolean = true;
|
||||
@property({ type: Boolean }) public showTopbar: boolean = false;
|
||||
@property() public topbarClass?: string;
|
||||
@property() public datetime?: string;
|
||||
@property() public iconTitle?: string;
|
||||
@property() public iconClass?: string;
|
||||
@property() public icon = "hass:help-circle";
|
||||
@property() public iconImage?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<iron-icon
|
||||
class=${this.iconClass}
|
||||
.icon=${this.icon}
|
||||
.title=${this.iconTitle}
|
||||
></iron-icon>
|
||||
${this.showTopbar
|
||||
? html`
|
||||
<div class="topbar ${this.topbarClass}"></div>
|
||||
`
|
||||
: ""}
|
||||
${this.iconImage
|
||||
? html`
|
||||
<div class="icon_image ${this.iconClass}">
|
||||
<img src="${this.iconImage}" title="${this.iconTitle}" />
|
||||
<div></div>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<iron-icon
|
||||
class=${this.iconClass}
|
||||
.icon=${this.icon}
|
||||
.title=${this.iconTitle}
|
||||
></iron-icon>
|
||||
`}
|
||||
<div>
|
||||
<div class="title">${this.title}</div>
|
||||
<div class="title">
|
||||
${this.title}
|
||||
</div>
|
||||
<div class="addition">
|
||||
${this.description}
|
||||
${/* treat as available when undefined */
|
||||
@@ -53,8 +72,9 @@ class HassioCardContent extends LitElement {
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
iron-icon {
|
||||
margin-right: 16px;
|
||||
margin-top: 16px;
|
||||
margin-right: 24px;
|
||||
margin-left: 8px;
|
||||
margin-top: 12px;
|
||||
float: left;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
@@ -88,6 +108,44 @@ class HassioCardContent extends LitElement {
|
||||
ha-relative-time {
|
||||
display: block;
|
||||
}
|
||||
.icon_image img {
|
||||
max-height: 40px;
|
||||
max-width: 40px;
|
||||
margin-top: 4px;
|
||||
margin-right: 16px;
|
||||
float: left;
|
||||
}
|
||||
.icon_image.stopped,
|
||||
.icon_image.not_available {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
.dot {
|
||||
position: absolute;
|
||||
background-color: var(--paper-orange-400);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.topbar {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
.topbar.installed {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
.topbar.update {
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
.topbar.unavailable {
|
||||
background-color: var(--error-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ import { navigate } from "../../../src/common/navigate";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import "../components/hassio-card-content";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
|
||||
@customElement("hassio-addons")
|
||||
class HassioAddons extends LitElement {
|
||||
@@ -23,37 +24,61 @@ class HassioAddons extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="content card-group">
|
||||
<div class="title">Add-ons</div>
|
||||
${!this.addons
|
||||
? html`
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
You don't have any add-ons installed yet. Head over to
|
||||
<a href="#" @click=${this._openStore}>the add-on store</a> to
|
||||
get started!
|
||||
</div>
|
||||
</paper-card>
|
||||
`
|
||||
: this.addons
|
||||
.sort((a, b) => (a.name > b.name ? 1 : -1))
|
||||
.map(
|
||||
(addon) => html`
|
||||
<paper-card .addon=${addon} @click=${this._addonTapped}>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
title=${addon.name}
|
||||
description=${addon.description}
|
||||
?available=${addon.available}
|
||||
icon=${this._computeIcon(addon)}
|
||||
.iconTitle=${this._computeIconTitle(addon)}
|
||||
.iconClass=${this._computeIconClass(addon)}
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
</paper-card>
|
||||
`
|
||||
)}
|
||||
<div class="content">
|
||||
<h1>Add-ons</h1>
|
||||
<div class="card-group">
|
||||
${!this.addons
|
||||
? html`
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
You don't have any add-ons installed yet. Head over to
|
||||
<a href="#" @click=${this._openStore}>the add-on store</a>
|
||||
to get started!
|
||||
</div>
|
||||
</paper-card>
|
||||
`
|
||||
: this.addons
|
||||
.sort((a, b) => (a.name > b.name ? 1 : -1))
|
||||
.map(
|
||||
(addon) => html`
|
||||
<paper-card .addon=${addon} @click=${this._addonTapped}>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${addon.name}
|
||||
.description=${addon.description}
|
||||
available
|
||||
.showTopbar=${addon.installed !== addon.version}
|
||||
topbarClass="update"
|
||||
.icon=${addon.installed !== addon.version
|
||||
? "hassio:arrow-up-bold-circle"
|
||||
: "hassio:puzzle"}
|
||||
.iconTitle=${addon.state !== "started"
|
||||
? "Add-on is stopped"
|
||||
: addon.installed !== addon.version
|
||||
? "New version available"
|
||||
: "Add-on is running"}
|
||||
.iconClass=${addon.installed &&
|
||||
addon.installed !== addon.version
|
||||
? addon.state === "started"
|
||||
? "update"
|
||||
: "update stopped"
|
||||
: addon.installed && addon.state === "started"
|
||||
? "running"
|
||||
: "stopped"}
|
||||
.iconImage=${atLeastVersion(
|
||||
this.hass.connection.haVersion,
|
||||
0,
|
||||
105
|
||||
) && addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined}
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
</paper-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -70,28 +95,6 @@ class HassioAddons extends LitElement {
|
||||
];
|
||||
}
|
||||
|
||||
private _computeIcon(addon: HassioAddonInfo): string {
|
||||
return addon.installed !== addon.version
|
||||
? "hassio:arrow-up-bold-circle"
|
||||
: "hassio:puzzle";
|
||||
}
|
||||
|
||||
private _computeIconTitle(addon: HassioAddonInfo): string {
|
||||
if (addon.installed !== addon.version) {
|
||||
return "New version available";
|
||||
}
|
||||
return addon.state === "started"
|
||||
? "Add-on is running"
|
||||
: "Add-on is stopped";
|
||||
}
|
||||
|
||||
private _computeIconClass(addon: HassioAddonInfo): string {
|
||||
if (addon.installed !== addon.version) {
|
||||
return "update";
|
||||
}
|
||||
return addon.state === "started" ? "running" : "";
|
||||
}
|
||||
|
||||
private _addonTapped(ev: any): void {
|
||||
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`);
|
||||
}
|
||||
|
@@ -38,7 +38,14 @@ export class HassioUpdate extends LitElement {
|
||||
this.supervisorInfo,
|
||||
this.hassOsInfo,
|
||||
].filter((value) => {
|
||||
return !!value && value.version !== value.last_version;
|
||||
return (
|
||||
!!value &&
|
||||
(value.last_version
|
||||
? value.version !== value.last_version
|
||||
: value.version_latest
|
||||
? value.version !== value.version_latest
|
||||
: false)
|
||||
);
|
||||
}).length;
|
||||
|
||||
if (!updatesAvailable) {
|
||||
@@ -52,14 +59,14 @@ export class HassioUpdate extends LitElement {
|
||||
<div class="error">Error: ${this._error}</div>
|
||||
`
|
||||
: ""}
|
||||
<h1>
|
||||
${updatesAvailable > 1
|
||||
? "Updates Available 🎉"
|
||||
: "Update Available 🎉"}
|
||||
</h1>
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
${updatesAvailable > 1
|
||||
? "Updates Available 🎉"
|
||||
: "Update Available 🎉"}
|
||||
</div>
|
||||
${this._renderUpdateCard(
|
||||
"Home Assistant",
|
||||
"Home Assistant Core",
|
||||
this.hassInfo.version,
|
||||
this.hassInfo.last_version,
|
||||
"hassio/homeassistant/update",
|
||||
@@ -69,7 +76,7 @@ export class HassioUpdate extends LitElement {
|
||||
"hassio:home-assistant"
|
||||
)}
|
||||
${this._renderUpdateCard(
|
||||
"Hass.io Supervisor",
|
||||
"Supervisor",
|
||||
this.supervisorInfo.version,
|
||||
this.supervisorInfo.last_version,
|
||||
"hassio/supervisor/update",
|
||||
@@ -77,7 +84,7 @@ export class HassioUpdate extends LitElement {
|
||||
)}
|
||||
${this.hassOsInfo
|
||||
? this._renderUpdateCard(
|
||||
"HassOS",
|
||||
"Operating System",
|
||||
this.hassOsInfo.version,
|
||||
this.hassOsInfo.version_latest,
|
||||
"hassio/hassos/update",
|
||||
@@ -97,7 +104,7 @@ export class HassioUpdate extends LitElement {
|
||||
releaseNotesUrl: string,
|
||||
icon?: string
|
||||
): TemplateResult {
|
||||
if (lastVersion === curVersion) {
|
||||
if (!lastVersion || lastVersion === curVersion) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
@@ -116,7 +123,7 @@ export class HassioUpdate extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a href="${releaseNotesUrl}" target="_blank">
|
||||
<a href="${releaseNotesUrl}" target="_blank" rel="noreferrer">
|
||||
<mwc-button>Release notes</mwc-button>
|
||||
</a>
|
||||
<ha-call-api-button
|
||||
@@ -149,13 +156,6 @@ export class HassioUpdate extends LitElement {
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host {
|
||||
width: 33%;
|
||||
}
|
||||
paper-card {
|
||||
display: inline-block;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.icon {
|
||||
--iron-icon-height: 48px;
|
||||
--iron-icon-width: 48px;
|
||||
@@ -170,6 +170,10 @@ export class HassioUpdate extends LitElement {
|
||||
.warning {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.card-content {
|
||||
height: calc(100% - 47px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.card-actions {
|
||||
text-align: right;
|
||||
}
|
||||
|
@@ -1,9 +1,12 @@
|
||||
window.loadES5Adapter().then(() => {
|
||||
// eslint-disable-next-line
|
||||
import(/* webpackChunkName: "roboto" */ "../../src/resources/roboto");
|
||||
// eslint-disable-next-line
|
||||
import(/* webpackChunkName: "hassio-icons" */ "./resources/hassio-icons");
|
||||
// eslint-disable-next-line
|
||||
import(/* webpackChunkName: "hassio-main" */ "./hassio-main");
|
||||
});
|
||||
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.innerHTML = `
|
||||
body {
|
||||
|
@@ -30,6 +30,10 @@ import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
||||
// Don't codesplit it, that way the dashboard always loads fast.
|
||||
import "./hassio-pages-with-tabs";
|
||||
import { navigate } from "../../src/common/navigate";
|
||||
import {
|
||||
showAlertDialog,
|
||||
AlertDialogParams,
|
||||
} from "../../src/dialogs/generic/show-dialog-box";
|
||||
|
||||
// The register callback of the IronA11yKeysBehavior inside paper-icon-button
|
||||
// is not called, causing _keyBindings to be uninitiliazed for paper-icon-button,
|
||||
@@ -72,7 +76,6 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@property() private _supervisorInfo: HassioSupervisorInfo;
|
||||
@property() private _hostInfo: HassioHostInfo;
|
||||
@property() private _hassOsInfo?: HassioHassOSInfo;
|
||||
@@ -81,7 +84,12 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
applyThemesOnElement(this, this.hass.themes, this.hass.selectedTheme, true);
|
||||
applyThemesOnElement(
|
||||
this.parentElement,
|
||||
this.hass.themes,
|
||||
this.hass.selectedTheme,
|
||||
true
|
||||
);
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
// Paulus - March 17, 2019
|
||||
// We went to a single hass-toggle-menu event in HA 0.90. However, the
|
||||
@@ -107,6 +115,14 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
||||
})
|
||||
);
|
||||
|
||||
// Forward haptic events to parent window.
|
||||
window.addEventListener("haptic", (ev) => {
|
||||
// @ts-ignore
|
||||
fireEvent(window.parent, ev.type, ev.detail, {
|
||||
bubbles: false,
|
||||
});
|
||||
});
|
||||
|
||||
makeDialogManager(this, document.body);
|
||||
}
|
||||
|
||||
@@ -158,31 +174,81 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
||||
}
|
||||
|
||||
private async _redirectIngress(addonSlug: string) {
|
||||
// When we trigger a navigation, we sleep to make sure we don't
|
||||
// show the hassio dashboard before navigating away.
|
||||
const awaitAlert = async (
|
||||
alertParams: AlertDialogParams,
|
||||
action: () => void
|
||||
) => {
|
||||
await new Promise((resolve) => {
|
||||
alertParams.confirm = resolve;
|
||||
showAlertDialog(this, alertParams);
|
||||
});
|
||||
action();
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
};
|
||||
|
||||
const createSessionPromise = createHassioSession(this.hass).then(
|
||||
() => true,
|
||||
() => false
|
||||
);
|
||||
|
||||
let addon;
|
||||
|
||||
try {
|
||||
const [addon] = await Promise.all([
|
||||
fetchHassioAddonInfo(this.hass, addonSlug).catch(() => {
|
||||
throw new Error("Failed to fetch add-on info");
|
||||
}),
|
||||
createHassioSession(this.hass).catch(() => {
|
||||
throw new Error("Failed to create an ingress session");
|
||||
}),
|
||||
]);
|
||||
if (!addon.ingress_url) {
|
||||
alert("Add-on does not support Ingress");
|
||||
return;
|
||||
}
|
||||
if (addon.state !== "started") {
|
||||
alert("Add-on is not running. Please start it first");
|
||||
navigate(this, `/hassio/addon/${addon.slug}`, true);
|
||||
return;
|
||||
}
|
||||
location.assign(addon.ingress_url);
|
||||
// await a promise that doesn't resolve, so we show the loading screen
|
||||
// while we load the next page.
|
||||
await new Promise(() => undefined);
|
||||
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
|
||||
} catch (err) {
|
||||
alert("Unable to open ingress connection");
|
||||
await awaitAlert(
|
||||
{
|
||||
text: "Unable to fetch add-on info to start Ingress",
|
||||
title: "Hass.io",
|
||||
},
|
||||
() => history.back()
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!addon.ingress_url) {
|
||||
await awaitAlert(
|
||||
{
|
||||
text: "Add-on does not support Ingress",
|
||||
title: addon.name,
|
||||
},
|
||||
() => history.back()
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (addon.state !== "started") {
|
||||
await awaitAlert(
|
||||
{
|
||||
text: "Add-on is not running. Please start it first",
|
||||
title: addon.name,
|
||||
},
|
||||
() => navigate(this, `/hassio/addon/${addon.slug}`, true)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await createSessionPromise)) {
|
||||
await awaitAlert(
|
||||
{
|
||||
text: "Unable to create an Ingress session",
|
||||
title: addon.name,
|
||||
},
|
||||
() => history.back()
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
location.assign(addon.ingress_url);
|
||||
// await a promise that doesn't resolve, so we show the loading screen
|
||||
// while we load the next page.
|
||||
await new Promise(() => undefined);
|
||||
}
|
||||
|
||||
private _apiCalled(ev) {
|
||||
|
@@ -52,7 +52,7 @@ class HassioPagesWithTabs extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
hassio
|
||||
></ha-menu-button>
|
||||
<div main-title>Hass.io</div>
|
||||
<div main-title>Supervisor</div>
|
||||
${HAS_REFRESH_BUTTON.includes(page)
|
||||
? html`
|
||||
<paper-icon-button
|
||||
@@ -123,13 +123,9 @@ class HassioPagesWithTabs extends LitElement {
|
||||
}
|
||||
paper-tabs {
|
||||
margin-left: 12px;
|
||||
--paper-tabs-selection-bar-color: #fff;
|
||||
--paper-tabs-selection-bar-color: var(--text-primary-color, #fff);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
app-header,
|
||||
app-toolbar {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -1,48 +1,43 @@
|
||||
import { css } from "lit-element";
|
||||
|
||||
const documentContainer = document.createElement("template");
|
||||
documentContainer.setAttribute("style", "display: none;");
|
||||
|
||||
export const hassioStyle = css`
|
||||
.card-group {
|
||||
margin-top: 24px;
|
||||
.content {
|
||||
margin: 8px;
|
||||
}
|
||||
.card-group .title {
|
||||
h1 {
|
||||
color: var(--primary-text-color);
|
||||
font-size: 2em;
|
||||
padding-left: 8px;
|
||||
margin-bottom: 8px;
|
||||
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);
|
||||
padding-left: 8px;
|
||||
}
|
||||
.card-group .description {
|
||||
font-size: 0.5em;
|
||||
font-weight: 500;
|
||||
.description {
|
||||
margin-top: 4px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
.card-group paper-card {
|
||||
--card-group-columns: 4;
|
||||
width: calc(
|
||||
(100% - 12px * var(--card-group-columns)) / var(--card-group-columns)
|
||||
);
|
||||
margin: 4px;
|
||||
vertical-align: top;
|
||||
.card-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
grid-gap: 8px;
|
||||
}
|
||||
@media screen and (max-width: 1200px) and (min-width: 901px) {
|
||||
.card-group paper-card {
|
||||
--card-group-columns: 3;
|
||||
@media screen and (min-width: 640px) {
|
||||
.card-group {
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 0.5fr));
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 900px) and (min-width: 601px) {
|
||||
.card-group paper-card {
|
||||
--card-group-columns: 2;
|
||||
@media screen and (min-width: 1020px) {
|
||||
.card-group {
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 0.333fr));
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 600px) and (min-width: 0) {
|
||||
.card-group paper-card {
|
||||
width: 100%;
|
||||
margin: 4px 0;
|
||||
}
|
||||
.content {
|
||||
padding: 0;
|
||||
@media screen and (min-width: 1300px) {
|
||||
.card-group {
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 0.25fr));
|
||||
}
|
||||
}
|
||||
ha-call-api-button {
|
||||
@@ -50,17 +45,7 @@ export const hassioStyle = css`
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.error {
|
||||
color: var(--google-red-500);
|
||||
color: var(--error-color);
|
||||
margin-top: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
documentContainer.innerHTML = `<dom-module id="hassio-style">
|
||||
<template>
|
||||
<style>
|
||||
${hassioStyle.toString()}
|
||||
</style>
|
||||
</template>
|
||||
</dom-module>`;
|
||||
|
||||
document.head.appendChild(documentContainer.content);
|
||||
|
@@ -79,14 +79,14 @@ class HassioSnapshots extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="content">
|
||||
<h1>
|
||||
Create snapshot
|
||||
</h1>
|
||||
<p class="description">
|
||||
Snapshots allow you to easily backup and restore all data of your Home
|
||||
Assistant instance.
|
||||
</p>
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
Create snapshot
|
||||
<div class="description">
|
||||
Snapshots allow you to easily backup and restore all data of your
|
||||
Hass.io instance.
|
||||
</div>
|
||||
</div>
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<paper-input
|
||||
@@ -173,8 +173,8 @@ class HassioSnapshots extends LitElement {
|
||||
</paper-card>
|
||||
</div>
|
||||
|
||||
<h1>Available snapshots</h1>
|
||||
<div class="card-group">
|
||||
<div class="title">Available snapshots</div>
|
||||
${this._snapshots === undefined
|
||||
? undefined
|
||||
: this._snapshots.length === 0
|
||||
|
@@ -126,23 +126,13 @@ class HassioHostInfo extends LitElement {
|
||||
hassioStyle,
|
||||
css`
|
||||
paper-card {
|
||||
display: inline-block;
|
||||
width: 400px;
|
||||
margin-left: 8px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.card-content {
|
||||
height: 200px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
@media screen and (max-width: 830px) {
|
||||
paper-card {
|
||||
margin-top: 8px;
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.card-content {
|
||||
height: auto;
|
||||
}
|
||||
box-sizing: border-box;
|
||||
height: calc(100% - 47px);
|
||||
}
|
||||
.info {
|
||||
width: 100%;
|
||||
|
@@ -32,7 +32,7 @@ class HassioSupervisorInfo extends LitElement {
|
||||
return html`
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<h2>Hass.io supervisor</h2>
|
||||
<h2>Supervisor</h2>
|
||||
<table class="info">
|
||||
<tbody>
|
||||
<tr>
|
||||
@@ -103,20 +103,13 @@ class HassioSupervisorInfo extends LitElement {
|
||||
hassioStyle,
|
||||
css`
|
||||
paper-card {
|
||||
display: inline-block;
|
||||
width: 400px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.card-content {
|
||||
height: 200px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
@media screen and (max-width: 830px) {
|
||||
paper-card {
|
||||
width: 100%;
|
||||
}
|
||||
.card-content {
|
||||
height: auto;
|
||||
}
|
||||
box-sizing: border-box;
|
||||
height: calc(100% - 47px);
|
||||
}
|
||||
.info {
|
||||
width: 100%;
|
||||
@@ -155,7 +148,7 @@ class HassioSupervisorInfo extends LitElement {
|
||||
!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.
|
||||
|
||||
This inludes beta releases for:
|
||||
This includes beta releases for:
|
||||
- Home Assistant (Release Candidates)
|
||||
- Hass.io supervisor
|
||||
- Host system`)
|
||||
|
@@ -50,6 +50,9 @@ class HassioSupervisorLog extends LitElement {
|
||||
hassioStyle,
|
||||
ANSI_HTML_STYLE,
|
||||
css`
|
||||
paper-card {
|
||||
width: 100%;
|
||||
}
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
@@ -32,17 +32,19 @@ class HassioSystem extends LitElement {
|
||||
public render(): TemplateResult | void {
|
||||
return html`
|
||||
<div class="content">
|
||||
<div class="title">Information</div>
|
||||
<hassio-supervisor-info
|
||||
.hass=${this.hass}
|
||||
.supervisorInfo=${this.supervisorInfo}
|
||||
></hassio-supervisor-info>
|
||||
<hassio-host-info
|
||||
.hass=${this.hass}
|
||||
.hostInfo=${this.hostInfo}
|
||||
.hassOsInfo=${this.hassOsInfo}
|
||||
></hassio-host-info>
|
||||
<div class="title">System log</div>
|
||||
<h1>Information</h1>
|
||||
<div class="card-group">
|
||||
<hassio-supervisor-info
|
||||
.hass=${this.hass}
|
||||
.supervisorInfo=${this.supervisorInfo}
|
||||
></hassio-supervisor-info>
|
||||
<hassio-host-info
|
||||
.hass=${this.hass}
|
||||
.hostInfo=${this.hostInfo}
|
||||
.hassOsInfo=${this.hassOsInfo}
|
||||
></hassio-host-info>
|
||||
</div>
|
||||
<h1>System log</h1>
|
||||
<hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log>
|
||||
</div>
|
||||
`;
|
||||
@@ -54,7 +56,7 @@ class HassioSystem extends LitElement {
|
||||
hassioStyle,
|
||||
css`
|
||||
.content {
|
||||
margin: 4px;
|
||||
margin: 8px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.title {
|
||||
@@ -64,6 +66,9 @@ class HassioSystem extends LitElement {
|
||||
padding-left: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
hassio-supervisor-log {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
65
package.json
65
package.json
@@ -18,16 +18,14 @@
|
||||
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@material/chips": "^3.2.0",
|
||||
"@material/data-table": "^3.2.0",
|
||||
"@material/mwc-base": "^0.10.0",
|
||||
"@material/mwc-button": "^0.10.0",
|
||||
"@material/mwc-checkbox": "^0.10.0",
|
||||
"@material/mwc-dialog": "^0.10.0",
|
||||
"@material/mwc-fab": "^0.10.0",
|
||||
"@material/mwc-ripple": "^0.10.0",
|
||||
"@material/mwc-switch": "^0.10.0",
|
||||
"@mdi/svg": "4.8.95",
|
||||
"@material/chips": "^5.0.0",
|
||||
"@material/mwc-button": "^0.13.0",
|
||||
"@material/mwc-checkbox": "^0.13.0",
|
||||
"@material/mwc-dialog": "^0.13.0",
|
||||
"@material/mwc-fab": "^0.13.0",
|
||||
"@material/mwc-ripple": "^0.13.0",
|
||||
"@material/mwc-switch": "^0.13.0",
|
||||
"@mdi/svg": "4.9.95",
|
||||
"@polymer/app-layout": "^3.0.2",
|
||||
"@polymer/app-localize-behavior": "^3.0.1",
|
||||
"@polymer/app-route": "^3.0.2",
|
||||
@@ -70,8 +68,9 @@
|
||||
"@polymer/paper-tooltip": "^3.0.1",
|
||||
"@polymer/polymer": "3.1.0",
|
||||
"@thomasloven/round-slider": "0.3.7",
|
||||
"@vaadin/vaadin-combo-box": "^5.0.6",
|
||||
"@vaadin/vaadin-date-picker": "^4.0.3",
|
||||
"@types/resize-observer-browser": "^0.1.3",
|
||||
"@vaadin/vaadin-combo-box": "^5.0.10",
|
||||
"@vaadin/vaadin-date-picker": "^4.0.7",
|
||||
"@webcomponents/shadycss": "^1.9.0",
|
||||
"@webcomponents/webcomponentsjs": "^2.2.7",
|
||||
"chart.js": "~2.8.0",
|
||||
@@ -79,12 +78,13 @@
|
||||
"codemirror": "^5.49.0",
|
||||
"cpx": "^1.5.0",
|
||||
"deep-clone-simple": "^1.1.1",
|
||||
"deep-freeze": "^0.0.1",
|
||||
"es6-object-assign": "^1.1.0",
|
||||
"fecha": "^3.0.2",
|
||||
"fuse.js": "^3.4.4",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^0.12.4",
|
||||
"home-assistant-js-websocket": "^4.4.0",
|
||||
"home-assistant-js-websocket": "5.0.0",
|
||||
"intl-messageformat": "^2.2.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"leaflet": "^1.4.0",
|
||||
@@ -96,10 +96,12 @@
|
||||
"mdn-polyfills": "^5.16.0",
|
||||
"memoize-one": "^5.0.2",
|
||||
"moment": "^2.24.0",
|
||||
"node-vibrant": "^3.1.5",
|
||||
"preact": "^8.4.2",
|
||||
"preact-compat": "^3.18.4",
|
||||
"react-big-calendar": "^0.20.4",
|
||||
"regenerator-runtime": "^0.13.2",
|
||||
"resize-observer": "^1.0.0",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"superstruct": "^0.6.1",
|
||||
"tslib": "^1.10.0",
|
||||
@@ -108,16 +110,17 @@
|
||||
"xss": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.7.4",
|
||||
"@babel/plugin-external-helpers": "^7.7.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-proposal-decorators": "^7.7.4",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.7.4",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.7.4",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
|
||||
"@babel/plugin-transform-react-jsx": "^7.7.4",
|
||||
"@babel/preset-env": "^7.7.4",
|
||||
"@babel/preset-typescript": "^7.7.4",
|
||||
"@babel/core": "^7.8.4",
|
||||
"@babel/plugin-external-helpers": "^7.8.3",
|
||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||
"@babel/plugin-proposal-decorators": "^7.8.3",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.8.3",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.8.3",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-react-jsx": "^7.8.3",
|
||||
"@babel/preset-env": "^7.8.4",
|
||||
"@babel/preset-typescript": "^7.8.3",
|
||||
"@types/chai": "^4.1.7",
|
||||
"@types/chromecast-caf-receiver": "^3.0.12",
|
||||
"@types/chromecast-caf-sender": "^1.0.1",
|
||||
@@ -143,13 +146,11 @@
|
||||
"fs-extra": "^7.0.1",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-foreach": "^0.1.0",
|
||||
"gulp-hash": "^4.2.2",
|
||||
"gulp-hash-filename": "^2.0.1",
|
||||
"gulp-insert": "^0.5.0",
|
||||
"gulp-json-transform": "^0.4.6",
|
||||
"gulp-jsonminify": "^1.1.0",
|
||||
"gulp-merge-json": "^1.3.1",
|
||||
"gulp-rename": "^1.4.0",
|
||||
"gulp-rename": "^2.0.0",
|
||||
"gulp-zopfli-green": "^3.0.1",
|
||||
"html-loader": "^0.5.5",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
@@ -172,6 +173,8 @@
|
||||
"tslint-eslint-rules": "^5.4.0",
|
||||
"tslint-plugin-prettier": "^2.0.1",
|
||||
"typescript": "^3.7.2",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"vinyl-source-stream": "^2.0.0",
|
||||
"web-component-tester": "^6.9.2",
|
||||
"webpack": "^4.40.2",
|
||||
"webpack-cli": "^3.3.9",
|
||||
@@ -185,7 +188,15 @@
|
||||
"resolutions": {
|
||||
"@webcomponents/webcomponentsjs": "^2.2.10",
|
||||
"@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",
|
||||
"husky": {
|
||||
|
@@ -11,17 +11,13 @@
|
||||
"src/panels/dev-template/ha-panel-dev-template.js",
|
||||
"src/panels/history/ha-panel-history.js",
|
||||
"src/panels/iframe/ha-panel-iframe.js",
|
||||
"src/panels/kiosk/ha-panel-kiosk.js",
|
||||
"src/panels/logbook/ha-panel-logbook.js",
|
||||
"src/panels/map/ha-panel-map.js",
|
||||
"src/panels/shopping-list/ha-panel-shopping-list.js",
|
||||
"src/panels/mailbox/ha-panel-mailbox.js",
|
||||
"hassio/src/entrypoint.js"
|
||||
],
|
||||
"sources": [
|
||||
"src/**/*",
|
||||
"!src/translations/*"
|
||||
],
|
||||
"sources": ["src/**/*", "!src/translations/*"],
|
||||
"lint": {
|
||||
"rules": ["polymer-3"],
|
||||
"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(
|
||||
name="home-assistant-frontend",
|
||||
version="20200130.3",
|
||||
version="20200311.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
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 {
|
||||
type: "show_lovelace_view";
|
||||
viewPath: string | number | null;
|
||||
urlPath: string | null;
|
||||
}
|
||||
|
||||
export interface ShowDemoMessage extends BaseCastMessage {
|
||||
@@ -43,11 +44,13 @@ export const castSendAuth = (cast: CastManager, auth: Auth) =>
|
||||
|
||||
export const castSendShowLovelaceView = (
|
||||
cast: CastManager,
|
||||
viewPath: ShowLovelaceViewMessage["viewPath"]
|
||||
viewPath: ShowLovelaceViewMessage["viewPath"],
|
||||
urlPath?: string | null
|
||||
) =>
|
||||
cast.sendMessage({
|
||||
type: "show_lovelace_view",
|
||||
viewPath,
|
||||
urlPath: urlPath || null,
|
||||
});
|
||||
|
||||
export const castSendShowDemo = (cast: CastManager) =>
|
||||
|
@@ -8,6 +8,7 @@ export interface ReceiverStatusMessage extends BaseCastMessage {
|
||||
showDemo: boolean;
|
||||
hassUrl?: string;
|
||||
lovelacePath?: string | number | null;
|
||||
urlPath?: string | null;
|
||||
}
|
||||
|
||||
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",
|
||||
"lock",
|
||||
"media_player",
|
||||
"person",
|
||||
"script",
|
||||
"sun",
|
||||
"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 { toLocaleDateStringSupportsOptions } from "./check_options_support";
|
||||
|
||||
// Check for support of native locale string options
|
||||
function toLocaleDateStringSupportsOptions() {
|
||||
try {
|
||||
new Date().toLocaleDateString("i");
|
||||
} catch (e) {
|
||||
return e.name === "RangeError";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default toLocaleDateStringSupportsOptions()
|
||||
export const formatDate = toLocaleDateStringSupportsOptions
|
||||
? (dateObj: Date, locales: string) =>
|
||||
dateObj.toLocaleDateString(locales, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
: (dateObj: Date) => fecha.format(dateObj, "mediumDate");
|
||||
: (dateObj: Date) => fecha.format(dateObj, "longDate");
|
||||
|
@@ -1,16 +1,7 @@
|
||||
import fecha from "fecha";
|
||||
import { toLocaleStringSupportsOptions } from "./check_options_support";
|
||||
|
||||
// Check for support of native locale string options
|
||||
function toLocaleStringSupportsOptions() {
|
||||
try {
|
||||
new Date().toLocaleString("i");
|
||||
} catch (e) {
|
||||
return e.name === "RangeError";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default toLocaleStringSupportsOptions()
|
||||
export const formatDateTime = toLocaleStringSupportsOptions
|
||||
? (dateObj: Date, locales: string) =>
|
||||
dateObj.toLocaleString(locales, {
|
||||
year: "numeric",
|
||||
@@ -19,4 +10,24 @@ export default toLocaleStringSupportsOptions()
|
||||
hour: "numeric",
|
||||
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 { toLocaleTimeStringSupportsOptions } from "./check_options_support";
|
||||
|
||||
// Check for support of native locale string options
|
||||
function toLocaleTimeStringSupportsOptions() {
|
||||
try {
|
||||
new Date().toLocaleTimeString("i");
|
||||
} catch (e) {
|
||||
return e.name === "RangeError";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default toLocaleTimeStringSupportsOptions()
|
||||
export const formatTime = toLocaleTimeStringSupportsOptions
|
||||
? (dateObj: Date, locales: string) =>
|
||||
dateObj.toLocaleTimeString(locales, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: (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");
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import { derivedStyles } from "../../resources/styles";
|
||||
|
||||
const hexToRgb = (hex: string): string | null => {
|
||||
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
|
||||
const checkHex = hex.replace(shorthandRegex, (_m, r, g, b) => {
|
||||
@@ -36,7 +38,7 @@ export const applyThemesOnElement = (
|
||||
}
|
||||
const styles = { ...element._themes };
|
||||
if (themeName !== "default") {
|
||||
const theme = themes.themes[themeName];
|
||||
const theme = { ...derivedStyles, ...themes.themes[themeName] };
|
||||
Object.keys(theme).forEach((key) => {
|
||||
const prefixedKey = `--${key}`;
|
||||
element._themes[prefixedKey] = "";
|
||||
|
@@ -4,7 +4,7 @@ export const dynamicElement = directive(
|
||||
(tag: string, properties?: { [key: string]: any }) => (part: Part): void => {
|
||||
if (!(part instanceof NodePart)) {
|
||||
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 { computeStateDomain } from "./compute_state_domain";
|
||||
import formatDateTime from "../datetime/format_date_time";
|
||||
import formatDate from "../datetime/format_date";
|
||||
import formatTime from "../datetime/format_time";
|
||||
import { formatDateTime } from "../datetime/format_date_time";
|
||||
import { formatDate } from "../datetime/format_date";
|
||||
import { formatTime } from "../datetime/format_time";
|
||||
import { LocalizeFunc } from "../translations/localize";
|
||||
|
||||
export const computeStateDisplay = (
|
||||
|
@@ -23,7 +23,7 @@ const fixedIcons = {
|
||||
homeassistant: "hass:home-assistant",
|
||||
homekit: "hass:home-automation",
|
||||
image_processing: "hass:image-filter-frames",
|
||||
input_boolean: "hass:drawing",
|
||||
input_boolean: "hass:toggle-switch-outline",
|
||||
input_datetime: "hass:calendar-clock",
|
||||
input_number: "hass:ray-vertex",
|
||||
input_select: "hass:format-list-bulleted",
|
||||
|
@@ -7,14 +7,18 @@ import {
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../dom/fire_event";
|
||||
import "@polymer/iron-icon/iron-icon";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
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")
|
||||
class SearchInput extends LitElement {
|
||||
@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() {
|
||||
this.shadowRoot!.querySelector("paper-input")!.focus();
|
||||
@@ -22,18 +26,24 @@ class SearchInput extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
.no-underline {
|
||||
--paper-input-container-underline: {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div class="search-container">
|
||||
<paper-input
|
||||
class=${classMap({ "no-underline": this.noUnderline })}
|
||||
autofocus
|
||||
label="Search"
|
||||
.value=${this.filter}
|
||||
@value-changed=${this._filterInputChanged}
|
||||
.noLabelFloat=${this.noLabelFloat}
|
||||
>
|
||||
<iron-icon
|
||||
icon="hass:magnify"
|
||||
slot="prefix"
|
||||
class="prefix"
|
||||
></iron-icon>
|
||||
<ha-icon icon="hass:magnify" slot="prefix" class="prefix"></ha-icon>
|
||||
${this.filter &&
|
||||
html`
|
||||
<paper-icon-button
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { css } from "lit-element";
|
||||
|
||||
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="automation"][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);
|
||||
}
|
||||
|
||||
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="zwave"][data-state="dead"] {
|
||||
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 {
|
||||
MDCDataTableAdapter,
|
||||
MDCDataTableFoundation,
|
||||
} from "@material/data-table";
|
||||
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
|
||||
import { scroll } from "lit-virtualizer";
|
||||
|
||||
import {
|
||||
html,
|
||||
query,
|
||||
queryAll,
|
||||
CSSResult,
|
||||
css,
|
||||
customElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
PropertyValues,
|
||||
LitElement,
|
||||
} from "lit-element";
|
||||
|
||||
import { BaseElement } from "@material/mwc-base/base-element";
|
||||
|
||||
// eslint-disable-next-line import/no-webpack-loader-syntax
|
||||
// @ts-ignore
|
||||
// tslint:disable-next-line: no-implicit-dependencies
|
||||
@@ -35,6 +29,8 @@ import { HaCheckbox } from "../ha-checkbox";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@@ -50,8 +46,7 @@ export interface RowClickedEvent {
|
||||
}
|
||||
|
||||
export interface SelectionChangedEvent {
|
||||
id: string;
|
||||
selected: boolean;
|
||||
value: string[];
|
||||
}
|
||||
|
||||
export interface SortingChangedEvent {
|
||||
@@ -76,31 +71,34 @@ export interface DataTableColumnData extends DataTableSortColumnData {
|
||||
title: string;
|
||||
type?: "numeric" | "icon";
|
||||
template?: <T>(data: any, row: T) => TemplateResult | string;
|
||||
width?: string;
|
||||
grows?: boolean;
|
||||
}
|
||||
|
||||
export interface DataTableRowData {
|
||||
[key: string]: any;
|
||||
selectable?: boolean;
|
||||
}
|
||||
|
||||
@customElement("ha-data-table")
|
||||
export class HaDataTable extends BaseElement {
|
||||
export class HaDataTable extends LitElement {
|
||||
@property({ type: Object }) public columns: DataTableColumnContainer = {};
|
||||
@property({ type: Array }) public data: DataTableRowData[] = [];
|
||||
@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 noDataText?: string;
|
||||
@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 _headerChecked = false;
|
||||
@property({ type: Boolean }) private _headerIndeterminate = false;
|
||||
@property({ type: Array }) private _checkedRows: string[] = [];
|
||||
@property({ type: String }) private _filter = "";
|
||||
@property({ type: String }) private _sortColumn?: string;
|
||||
@property({ type: String }) private _sortDirection: SortingDirection = null;
|
||||
@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: {
|
||||
[key: string]: DataTableSortColumnData;
|
||||
} = {};
|
||||
@@ -111,18 +109,25 @@ export class HaDataTable extends BaseElement {
|
||||
(value: string) => {
|
||||
this._filter = value;
|
||||
},
|
||||
200,
|
||||
100,
|
||||
false
|
||||
);
|
||||
|
||||
public clearSelection(): void {
|
||||
this._headerChecked = false;
|
||||
this._headerIndeterminate = false;
|
||||
this.mdcFoundation.handleHeaderRowCheckboxChange();
|
||||
this._checkedRows = [];
|
||||
this._checkedRowsChanged();
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
super.firstUpdated();
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this._filteredData.length) {
|
||||
// Force update of location of rows
|
||||
this._filteredData = [...this._filteredData];
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(properties: PropertyValues) {
|
||||
super.firstUpdated(properties);
|
||||
this._worker = sortFilterWorker();
|
||||
}
|
||||
|
||||
@@ -156,6 +161,12 @@ export class HaDataTable extends BaseElement {
|
||||
this._debounceSearch(this.filter);
|
||||
}
|
||||
|
||||
if (properties.has("data")) {
|
||||
this._checkableRowsCount = this.data.filter(
|
||||
(row) => row.selectable !== false
|
||||
).length;
|
||||
}
|
||||
|
||||
if (
|
||||
properties.has("data") ||
|
||||
properties.has("columns") ||
|
||||
@@ -170,7 +181,7 @@ export class HaDataTable extends BaseElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="mdc-data-table">
|
||||
<slot name="header">
|
||||
<slot name="header" @slotchange=${this._calcTableHeight}>
|
||||
${this._filterable
|
||||
? html`
|
||||
<div class="table-header">
|
||||
@@ -181,158 +192,163 @@ export class HaDataTable extends BaseElement {
|
||||
`
|
||||
: ""}
|
||||
</slot>
|
||||
<table class="mdc-data-table__table">
|
||||
<thead>
|
||||
<tr class="mdc-data-table__header-row">
|
||||
${this.selectable
|
||||
? html`
|
||||
<th
|
||||
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@change=${this._handleHeaderRowCheckboxChange}
|
||||
.indeterminate=${this._headerIndeterminate}
|
||||
.checked=${this._headerChecked}
|
||||
>
|
||||
</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)}"
|
||||
<div
|
||||
class="mdc-data-table__table ${classMap({
|
||||
"auto-height": this.autoHeight,
|
||||
})}"
|
||||
style=${styleMap({
|
||||
height: this.autoHeight
|
||||
? `${(this._filteredData.length || 1) * 53 + 57}px`
|
||||
: `calc(100% - ${this._header?.clientHeight}px)`,
|
||||
})}
|
||||
>
|
||||
<div class="mdc-data-table__header-row">
|
||||
${this.selectable
|
||||
? html`
|
||||
<div
|
||||
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
@click=${this._handleHeaderClick}
|
||||
data-column-id="${key}"
|
||||
>
|
||||
${column.sortable
|
||||
? html`
|
||||
<ha-icon
|
||||
.icon=${sorted && this._sortDirection === "desc"
|
||||
? "hass:arrow-down"
|
||||
: "hass:arrow-up"}
|
||||
></ha-icon>
|
||||
`
|
||||
: ""}
|
||||
<span>${column.title}</span>
|
||||
</th>
|
||||
`;
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="mdc-data-table__content">
|
||||
${repeat(
|
||||
this._filteredData!,
|
||||
(row: DataTableRowData) => row[this.id],
|
||||
(row: DataTableRowData) => html`
|
||||
<tr
|
||||
data-row-id="${row[this.id]}"
|
||||
@click=${this._handleRowClick}
|
||||
class="mdc-data-table__row"
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@change=${this._handleHeaderRowCheckboxClick}
|
||||
.indeterminate=${this._checkedRows.length &&
|
||||
this._checkedRows.length !== this._checkableRowsCount}
|
||||
.checked=${this._checkedRows.length ===
|
||||
this._checkableRowsCount}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${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),
|
||||
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}
|
||||
>
|
||||
${this.selectable
|
||||
${column.sortable
|
||||
? html`
|
||||
<td
|
||||
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
|
||||
>
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@change=${this._handleRowCheckboxChange}
|
||||
.checked=${this._checkedRows.includes(
|
||||
String(row[this.id])
|
||||
)}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</td>
|
||||
<ha-icon
|
||||
.icon=${sorted && this._sortDirection === "desc"
|
||||
? "hass:arrow-down"
|
||||
: "hass:arrow-up"}
|
||||
></ha-icon>
|
||||
`
|
||||
: ""}
|
||||
${Object.entries(this.columns).map((columnEntry) => {
|
||||
const [key, column] = columnEntry;
|
||||
return html`
|
||||
<td
|
||||
class="mdc-data-table__cell ${classMap({
|
||||
"mdc-data-table__cell--numeric": Boolean(
|
||||
column.type && column.type === "numeric"
|
||||
),
|
||||
"mdc-data-table__cell--icon": Boolean(
|
||||
column.type && column.type === "icon"
|
||||
<span>${column.title}</span>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
${!this._filteredData.length
|
||||
? html`
|
||||
<div class="mdc-data-table__content">
|
||||
<div class="mdc-data-table__row">
|
||||
<div class="mdc-data-table__cell grows center">
|
||||
${this.noDataText || "No data"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="mdc-data-table__content scroller">
|
||||
${scroll({
|
||||
items: this._filteredData,
|
||||
renderItem: (row: DataTableRowData) => html`
|
||||
<div
|
||||
.rowId="${row[this.id]}"
|
||||
@click=${this._handleRowClick}
|
||||
class="mdc-data-table__row ${classMap({
|
||||
"mdc-data-table__row--selected": this._checkedRows.includes(
|
||||
String(row[this.id])
|
||||
),
|
||||
})}"
|
||||
aria-selected=${ifDefined(
|
||||
this._checkedRows.includes(String(row[this.id]))
|
||||
? true
|
||||
: undefined
|
||||
)}
|
||||
.selectable=${row.selectable !== false}
|
||||
>
|
||||
${column.template
|
||||
? column.template(row[key], row)
|
||||
: row[key]}
|
||||
</td>
|
||||
`;
|
||||
${this.selectable
|
||||
? html`
|
||||
<div
|
||||
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
|
||||
>
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@change=${this._handleRowCheckboxClick}
|
||||
.disabled=${row.selectable === false}
|
||||
.checked=${this._checkedRows.includes(
|
||||
String(row[this.id])
|
||||
)}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${Object.entries(this.columns).map((columnEntry) => {
|
||||
const [key, column] = columnEntry;
|
||||
return html`
|
||||
<div
|
||||
class="mdc-data-table__cell ${classMap({
|
||||
"mdc-data-table__cell--numeric": Boolean(
|
||||
column.type && column.type === "numeric"
|
||||
),
|
||||
"mdc-data-table__cell--icon": Boolean(
|
||||
column.type && column.type === "icon"
|
||||
),
|
||||
grows: Boolean(column.grows),
|
||||
})}"
|
||||
style=${column.width
|
||||
? styleMap({
|
||||
[column.grows
|
||||
? "minWidth"
|
||||
: "width"]: String(column.width),
|
||||
})
|
||||
: ""}
|
||||
>
|
||||
${column.template
|
||||
? column.template(row[key], row)
|
||||
: row[key]}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`,
|
||||
})}
|
||||
</tr>
|
||||
`
|
||||
)}
|
||||
</tbody>
|
||||
</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() {
|
||||
const startTime = new Date().getTime();
|
||||
this.curRequest++;
|
||||
@@ -360,14 +376,10 @@ export class HaDataTable extends BaseElement {
|
||||
this._filteredData = data;
|
||||
}
|
||||
|
||||
private _getRowIdAtIndex(rowIndex: number): string {
|
||||
return this.rowElements[rowIndex].getAttribute("data-row-id")!;
|
||||
}
|
||||
|
||||
private _handleHeaderClick(ev: Event) {
|
||||
const columnId = (ev.target as HTMLElement)
|
||||
.closest("th")!
|
||||
.getAttribute("data-column-id")!;
|
||||
const columnId = ((ev.target as HTMLElement).closest(
|
||||
".mdc-data-table__header-cell"
|
||||
) as any).columnId;
|
||||
if (!this.columns[columnId].sortable) {
|
||||
return;
|
||||
}
|
||||
@@ -387,19 +399,32 @@ export class HaDataTable extends BaseElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleHeaderRowCheckboxChange(ev: Event) {
|
||||
private _handleHeaderRowCheckboxClick(ev: Event) {
|
||||
const checkbox = ev.target as HaCheckbox;
|
||||
this._headerChecked = checkbox.checked;
|
||||
this._headerIndeterminate = checkbox.indeterminate;
|
||||
this.mdcFoundation.handleHeaderRowCheckboxChange();
|
||||
if (checkbox.checked) {
|
||||
this._checkedRows = this._filteredData
|
||||
.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 rowId = checkbox.closest("tr")!.getAttribute("data-row-id");
|
||||
const rowId = (checkbox.closest(".mdc-data-table__row") as any).rowId;
|
||||
|
||||
this._setRowChecked(rowId!, checkbox.checked);
|
||||
this.mdcFoundation.handleRowCheckboxChange(ev);
|
||||
if (checkbox.checked) {
|
||||
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) {
|
||||
@@ -407,26 +432,15 @@ export class HaDataTable extends BaseElement {
|
||||
if (target.tagName === "HA-CHECKBOX") {
|
||||
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 });
|
||||
}
|
||||
|
||||
private _setRowChecked(rowId: string, checked: boolean) {
|
||||
if (checked) {
|
||||
if (this._checkedRows.includes(rowId)) {
|
||||
return;
|
||||
}
|
||||
this._checkedRows = [...this._checkedRows, rowId];
|
||||
} else {
|
||||
const index = this._checkedRows.indexOf(rowId);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
this._checkedRows.splice(index, 1);
|
||||
}
|
||||
private _checkedRowsChanged() {
|
||||
// force scroller to update, change it's items
|
||||
this._filteredData = [...this._filteredData];
|
||||
fireEvent(this, "selection-changed", {
|
||||
id: rowId,
|
||||
selected: checked,
|
||||
value: this._checkedRows,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -434,10 +448,20 @@ export class HaDataTable extends BaseElement {
|
||||
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 {
|
||||
return css`
|
||||
/* default mdc styles, colors changed, without checkbox styles */
|
||||
|
||||
:host {
|
||||
height: 100%;
|
||||
}
|
||||
.mdc-data-table__content {
|
||||
font-family: Roboto, sans-serif;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@@ -459,7 +483,7 @@ export class HaDataTable extends BaseElement {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
overflow-x: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mdc-data-table__row--selected {
|
||||
@@ -467,12 +491,13 @@ export class HaDataTable extends BaseElement {
|
||||
}
|
||||
|
||||
.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 {
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
.mdc-data-table__row ~ .mdc-data-table__row {
|
||||
border-top: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
||||
}
|
||||
|
||||
.mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
|
||||
@@ -489,16 +514,24 @@ export class HaDataTable extends BaseElement {
|
||||
|
||||
.mdc-data-table__header-row {
|
||||
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 {
|
||||
height: 52px;
|
||||
.mdc-data-table__header-row::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell,
|
||||
.mdc-data-table__header-cell {
|
||||
padding-right: 16px;
|
||||
padding-left: 16px;
|
||||
align-self: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell--checkbox,
|
||||
@@ -507,6 +540,7 @@ export class HaDataTable extends BaseElement {
|
||||
padding-left: 16px;
|
||||
/* @noflip */
|
||||
padding-right: 0;
|
||||
width: 40px;
|
||||
}
|
||||
[dir="rtl"] .mdc-data-table__header-cell--checkbox,
|
||||
.mdc-data-table__header-cell--checkbox[dir="rtl"],
|
||||
@@ -519,10 +553,10 @@ export class HaDataTable extends BaseElement {
|
||||
}
|
||||
|
||||
.mdc-data-table__table {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
white-space: nowrap;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell {
|
||||
@@ -551,6 +585,27 @@ export class HaDataTable extends BaseElement {
|
||||
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 {
|
||||
font-family: Roboto, sans-serif;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@@ -572,48 +627,83 @@ export class HaDataTable extends BaseElement {
|
||||
.mdc-data-table__header-cell--numeric {
|
||||
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,
|
||||
.mdc-data-table__header-cell--numeric[dir="rtl"] {
|
||||
/* @noflip */
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell--icon {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* custom from here */
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mdc-data-table {
|
||||
display: block;
|
||||
border-width: var(--data-table-border-width, 1px);
|
||||
height: 100%;
|
||||
}
|
||||
.mdc-data-table__header-cell {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.mdc-data-table__header-cell span {
|
||||
position: relative;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell.sortable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__header-cell--numeric):not(.mdc-data-table__header-cell--icon)
|
||||
span {
|
||||
position: relative;
|
||||
left: -24px;
|
||||
.mdc-data-table__header-cell > * {
|
||||
transition: left 0.2s ease;
|
||||
}
|
||||
.mdc-data-table__header-cell.not-sorted > * {
|
||||
transition: left 0.2s ease 0s;
|
||||
.mdc-data-table__header-cell ha-icon {
|
||||
top: -3px;
|
||||
position: absolute;
|
||||
}
|
||||
.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
|
||||
span {
|
||||
left: 0px;
|
||||
.mdc-data-table__header-cell.sortable:not(.not-sorted) span,
|
||||
.mdc-data-table__header-cell.sortable.not-sorted:hover span {
|
||||
left: 24px;
|
||||
}
|
||||
.mdc-data-table__header-cell:hover.not-sorted ha-icon {
|
||||
left: 0px;
|
||||
.mdc-data-table__header-cell.sortable:not(.not-sorted) ha-icon,
|
||||
.mdc-data-table__header-cell.sortable:hover.not-sorted ha-icon {
|
||||
left: 12px;
|
||||
}
|
||||
.table-header {
|
||||
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 {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
DeviceEntityLookup,
|
||||
} from "../../data/device_registry";
|
||||
import { compare } from "../../common/string/compare";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
@@ -30,7 +31,6 @@ import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../../data/area_registry";
|
||||
import { DeviceEntityLookup } from "../../panels/config/devices/ha-devices-data-table";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
|
@@ -22,6 +22,7 @@ import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
computeDeviceName,
|
||||
DeviceEntityLookup,
|
||||
} from "../../data/device_registry";
|
||||
import { compare } from "../../common/string/compare";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
@@ -29,7 +30,6 @@ import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../../data/area_registry";
|
||||
import { DeviceEntityLookup } from "../../panels/config/devices/ha-devices-data-table";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
|
@@ -6,7 +6,7 @@ import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
|
||||
import { timeOut } from "@polymer/polymer/lib/utils/async";
|
||||
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
|
||||
/* global Chart moment Color */
|
||||
|
||||
|
@@ -10,6 +10,9 @@ import {
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
import hassAttributeUtil from "../util/hass-attributes-util";
|
||||
import { until } from "lit-html/directives/until";
|
||||
|
||||
let jsYamlPromise: Promise<typeof import("js-yaml")>;
|
||||
|
||||
@customElement("ha-attributes")
|
||||
class HaAttributes extends LitElement {
|
||||
@@ -32,7 +35,7 @@ class HaAttributes extends LitElement {
|
||||
<div class="data-entry">
|
||||
<div class="key">${attribute.replace(/_/g, " ")}</div>
|
||||
<div class="value">
|
||||
${this.formatAttributeValue(attribute)}
|
||||
${this.formatAttribute(attribute)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
@@ -63,6 +66,10 @@ class HaAttributes extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
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) {
|
||||
return "-";
|
||||
}
|
||||
const value = this.stateObj.attributes[attribute];
|
||||
return this.formatAttributeValue(value);
|
||||
}
|
||||
|
||||
private formatAttributeValue(value: any): string | TemplateResult {
|
||||
if (value === null) {
|
||||
return "-";
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(", ");
|
||||
if (
|
||||
(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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -5,8 +5,10 @@ import {
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
|
||||
@customElement("ha-card")
|
||||
class HaCard extends LitElement {
|
||||
@property() public header?: string;
|
||||
|
||||
@@ -70,4 +72,8 @@ class HaCard extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-card", HaCard);
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-card": HaCard;
|
||||
}
|
||||
}
|
||||
|
@@ -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")
|
||||
export class HaCheckbox extends MwcCheckbox {
|
||||
protected firstUpdated() {
|
||||
public firstUpdated() {
|
||||
super.firstUpdated();
|
||||
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ import {
|
||||
customElement,
|
||||
unsafeCSS,
|
||||
} from "lit-element";
|
||||
|
||||
import { ripple } from "@material/mwc-ripple/ripple-directive";
|
||||
// @ts-ignore
|
||||
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@@ -33,22 +33,27 @@ export class HaChips extends LitElement {
|
||||
${this.items.map(
|
||||
(item, idx) =>
|
||||
html`
|
||||
<button
|
||||
class="mdc-chip"
|
||||
.index=${idx}
|
||||
@click=${this._handleClick}
|
||||
>
|
||||
<span class="mdc-chip__text">${item}</span>
|
||||
</button>
|
||||
<div class="mdc-chip" .index=${idx} @click=${this._handleClick}>
|
||||
<div class="mdc-chip__ripple" .ripple="${ripple()}"></div>
|
||||
<span role="gridcell">
|
||||
<span
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="mdc-chip__primary-action"
|
||||
>
|
||||
<span class="mdc-chip__text">${item}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleClick(ev) {
|
||||
private _handleClick(ev): void {
|
||||
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 { style } from "@material/mwc-dialog/mwc-dialog-css";
|
||||
// tslint:disable-next-line
|
||||
import { Dialog } from "@material/mwc-dialog";
|
||||
import { Constructor } from "../types";
|
||||
import { Constructor, HomeAssistant } from "../types";
|
||||
// tslint:disable-next-line
|
||||
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")
|
||||
export class HaDialog extends MwcDialog {
|
||||
protected static get styles(): CSSResult[] {
|
||||
@@ -19,6 +30,15 @@ export class HaDialog extends MwcDialog {
|
||||
.mdc-dialog__container {
|
||||
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 { 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 { Constructor } from "../types";
|
||||
|
@@ -5,6 +5,8 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
query,
|
||||
CSSResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import {
|
||||
HaFormElement,
|
||||
@@ -19,13 +21,14 @@ import "@polymer/paper-input/paper-input";
|
||||
// tslint:disable-next-line
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { PaperSliderElement } from "@polymer/paper-slider/paper-slider";
|
||||
import { HaCheckbox } from "../ha-checkbox";
|
||||
|
||||
@customElement("ha-form-integer")
|
||||
export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
@property() public schema!: HaFormIntegerSchema;
|
||||
@property() public data!: HaFormIntegerData;
|
||||
@property() public label!: string;
|
||||
@property() public suffix!: string;
|
||||
@property() public data?: HaFormIntegerData;
|
||||
@property() public label?: string;
|
||||
@property() public suffix?: string;
|
||||
@query("paper-input ha-paper-slider") private _input?: HTMLElement;
|
||||
|
||||
public focus() {
|
||||
@@ -39,20 +42,31 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
? html`
|
||||
<div>
|
||||
${this.label}
|
||||
<ha-paper-slider
|
||||
pin=""
|
||||
.value=${this._value}
|
||||
.min=${this.schema.valueMin}
|
||||
.max=${this.schema.valueMax}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-paper-slider>
|
||||
<div class="flex">
|
||||
${this.schema.optional && this.schema.default === undefined
|
||||
? html`
|
||||
<ha-checkbox
|
||||
@change=${this._handleCheckboxChange}
|
||||
.checked=${this.data !== undefined}
|
||||
></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>
|
||||
`
|
||||
: html`
|
||||
<paper-input
|
||||
type="number"
|
||||
.label=${this.label}
|
||||
.value=${this.data}
|
||||
.value=${this._value}
|
||||
.required=${this.schema.required}
|
||||
.autoValidate=${this.schema.required}
|
||||
@value-changed=${this._valueChanged}
|
||||
@@ -61,7 +75,14 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -75,6 +96,14 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
TemplateResult,
|
||||
query,
|
||||
CSSResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -36,8 +38,10 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
.selected=${this.data}
|
||||
@selected-item-changed=${this._valueChanged}
|
||||
>
|
||||
${this.schema.options!.map(
|
||||
(item) => html`
|
||||
${// TS doesn't work with union array types https://github.com/microsoft/TypeScript/issues/36390
|
||||
// @ts-ignore
|
||||
this.schema.options!.map(
|
||||
(item: string | [string, string]) => html`
|
||||
<paper-item .itemValue=${this._optionValue(item)}>
|
||||
${this._optionLabel(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;
|
||||
}
|
||||
|
||||
private _optionLabel(item) {
|
||||
return Array.isArray(item) ? item[1] : item;
|
||||
private _optionLabel(item: string | [string, string]) {
|
||||
return Array.isArray(item) ? item[1] || item[0] : item;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
@@ -64,6 +68,14 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
value: ev.detail.value.itemValue,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
paper-dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -12,6 +12,7 @@ import "./ha-form-integer";
|
||||
import "./ha-form-float";
|
||||
import "./ha-form-boolean";
|
||||
import "./ha-form-select";
|
||||
import "./ha-form-multi_select";
|
||||
import "./ha-form-positive_time_period_dict";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
@@ -22,6 +23,7 @@ export type HaFormSchema =
|
||||
| HaFormFloatSchema
|
||||
| HaFormBooleanSchema
|
||||
| HaFormSelectSchema
|
||||
| HaFormMultiSelectSchema
|
||||
| HaFormTimeSchema;
|
||||
|
||||
export interface HaFormBaseSchema {
|
||||
@@ -41,7 +43,12 @@ export interface HaFormIntegerSchema extends HaFormBaseSchema {
|
||||
|
||||
export interface HaFormSelectSchema extends HaFormBaseSchema {
|
||||
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 {
|
||||
@@ -71,6 +78,7 @@ export type HaFormData =
|
||||
| HaFormFloatData
|
||||
| HaFormBooleanData
|
||||
| HaFormSelectData
|
||||
| HaFormMultiSelectData
|
||||
| HaFormTimeData;
|
||||
|
||||
export type HaFormStringData = string;
|
||||
@@ -78,6 +86,7 @@ export type HaFormIntegerData = number;
|
||||
export type HaFormFloatData = number;
|
||||
export type HaFormBooleanData = boolean;
|
||||
export type HaFormSelectData = string;
|
||||
export type HaFormMultiSelectData = string[];
|
||||
export interface HaFormTimeData {
|
||||
hours?: number;
|
||||
minutes?: number;
|
||||
@@ -86,7 +95,7 @@ export interface HaFormTimeData {
|
||||
|
||||
export interface HaFormElement extends LitElement {
|
||||
schema: HaFormSchema;
|
||||
data: HaFormDataContainer | HaFormData;
|
||||
data?: HaFormDataContainer | HaFormData;
|
||||
label?: 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.target = "_blank";
|
||||
node.rel = "noreferrer";
|
||||
|
||||
// protect referrer on external links and deny window.opener access for security reasons
|
||||
// (see https://mathiasbynens.github.io/rel-noopener/)
|
||||
|
@@ -18,6 +18,11 @@ class HaPaperSlider extends PaperSliderClass {
|
||||
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 {
|
||||
top: unset;
|
||||
margin-left: unset;
|
||||
|
@@ -68,6 +68,11 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
|
||||
if (!this._related) {
|
||||
return html``;
|
||||
}
|
||||
if (Object.keys(this._related).length === 0) {
|
||||
return html`
|
||||
${this.hass.localize("ui.components.related-items.no_related_found")}
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
${this._related.config_entry && this._entries
|
||||
? 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
|
||||
import { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { compare } from "../common/string/compare";
|
||||
|
||||
const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"];
|
||||
|
||||
@@ -46,7 +47,21 @@ const SORT_VALUE_URL_PATHS = {
|
||||
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 bBuiltIn = b.url_path in SORT_VALUE_URL_PATHS;
|
||||
|
||||
@@ -60,14 +75,9 @@ const panelSorter = (a, b) => {
|
||||
return 1;
|
||||
}
|
||||
// both not built in, sort by title
|
||||
if (a.title! < b.title!) {
|
||||
return -1;
|
||||
}
|
||||
if (a.title! > b.title!) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
return compare(a.title!, b.title!);
|
||||
};
|
||||
const DEFAULT_PAGE = localStorage.defaultPage || DEFAULT_PANEL;
|
||||
|
||||
const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => {
|
||||
const panels = hass.panels;
|
||||
@@ -79,7 +89,7 @@ const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => {
|
||||
const afterSpacer: PanelInfo[] = [];
|
||||
|
||||
Object.values(panels).forEach((panel) => {
|
||||
if (!panel.title) {
|
||||
if (!panel.title || panel.url_path === DEFAULT_PAGE) {
|
||||
return;
|
||||
}
|
||||
(SHOW_AFTER_SPACER.includes(panel.url_path)
|
||||
@@ -103,8 +113,7 @@ class HaSidebar extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public alwaysExpand = false;
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
@property() public _defaultPage?: string =
|
||||
localStorage.defaultPage || DEFAULT_PANEL;
|
||||
|
||||
@property() private _externalConfig?: ExternalConfig;
|
||||
@property() private _notifications?: PersistentNotification[];
|
||||
// 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`
|
||||
<div class="menu">
|
||||
${!this.narrow
|
||||
@@ -157,9 +169,9 @@ class HaSidebar extends LitElement {
|
||||
@keydown=${this._listboxKeydown}
|
||||
>
|
||||
${this._renderPanel(
|
||||
this._defaultPage,
|
||||
"hass:apps",
|
||||
hass.localize("panel.states")
|
||||
defaultPanel.url_path,
|
||||
defaultPanel.icon || "hass:view-dashboard",
|
||||
defaultPanel.title || hass.localize("panel.states")
|
||||
)}
|
||||
${beforeSpacer.map((panel) =>
|
||||
this._renderPanel(
|
||||
@@ -597,6 +609,7 @@ class HaSidebar extends LitElement {
|
||||
|
||||
paper-icon-item .item-text {
|
||||
display: none;
|
||||
max-width: calc(100% - 56px);
|
||||
}
|
||||
:host([expanded]) paper-icon-item .item-text {
|
||||
display: block;
|
||||
|
@@ -1,15 +1,27 @@
|
||||
import { customElement, CSSResult, css, query, html } from "lit-element";
|
||||
import {
|
||||
customElement,
|
||||
CSSResult,
|
||||
css,
|
||||
query,
|
||||
html,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import "@material/mwc-switch";
|
||||
import { style } from "@material/mwc-switch/mwc-switch-css";
|
||||
// tslint:disable-next-line
|
||||
import { Switch } from "@material/mwc-switch";
|
||||
import { Constructor } from "../types";
|
||||
import { forwardHaptic } from "../data/haptics";
|
||||
import { ripple } from "@material/mwc-ripple/ripple-directive";
|
||||
// tslint:disable-next-line
|
||||
const MwcSwitch = customElements.get("mwc-switch") as Constructor<Switch>;
|
||||
|
||||
@customElement("ha-switch")
|
||||
export class HaSwitch extends MwcSwitch {
|
||||
// Generate a haptic vibration.
|
||||
// Only set to true if the new value of the switch is applied right away when toggling.
|
||||
// Do not add haptic when a user is required to press save.
|
||||
@property({ type: Boolean }) public haptic = false;
|
||||
@query("slot") private _slot!: HTMLSlotElement;
|
||||
|
||||
protected firstUpdated() {
|
||||
@@ -22,6 +34,11 @@ export class HaSwitch extends MwcSwitch {
|
||||
"slotted",
|
||||
Boolean(this._slot.assignedNodes().length)
|
||||
);
|
||||
this.addEventListener("change", () => {
|
||||
if (this.haptic) {
|
||||
forwardHaptic("light");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected render() {
|
||||
|
@@ -6,6 +6,13 @@ import { afterNextRender } from "../common/util/render-status";
|
||||
// tslint:disable-next-line
|
||||
import { HaCodeEditor } from "./ha-code-editor";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"editor-refreshed": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const isEmpty = (obj: object) => {
|
||||
if (typeof obj !== "object") {
|
||||
return false;
|
||||
@@ -21,9 +28,10 @@ const isEmpty = (obj: object) => {
|
||||
@customElement("ha-yaml-editor")
|
||||
export class HaYamlEditor extends LitElement {
|
||||
@property() public value?: any;
|
||||
@property() public defaultValue?: any;
|
||||
@property() public isValid = true;
|
||||
@property() public label?: string;
|
||||
@property() private _yaml?: string;
|
||||
@property() private _yaml: string = "";
|
||||
@query("ha-code-editor") private _editor?: HaCodeEditor;
|
||||
|
||||
public setValue(value) {
|
||||
@@ -36,11 +44,14 @@ export class HaYamlEditor extends LitElement {
|
||||
if (this._editor?.codemirror) {
|
||||
this._editor.codemirror.refresh();
|
||||
}
|
||||
afterNextRender(() => fireEvent(this, "editor-refreshed"));
|
||||
});
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
this.setValue(this.value);
|
||||
if (this.defaultValue) {
|
||||
this.setValue(this.defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -71,7 +82,6 @@ export class HaYamlEditor extends LitElement {
|
||||
if (value) {
|
||||
try {
|
||||
parsed = safeLoad(value);
|
||||
isValid = true;
|
||||
} catch (err) {
|
||||
// Invalid YAML
|
||||
isValid = false;
|
||||
@@ -83,9 +93,7 @@ export class HaYamlEditor extends LitElement {
|
||||
this.value = parsed;
|
||||
this.isValid = isValid;
|
||||
|
||||
if (isValid) {
|
||||
fireEvent(this, "value-changed", { value: parsed });
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: parsed, isValid } as any);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -41,7 +41,8 @@ export interface MarkerLocation {
|
||||
id: string;
|
||||
icon?: string;
|
||||
radius_color?: string;
|
||||
editable?: boolean;
|
||||
location_editable?: boolean;
|
||||
radius_editable?: boolean;
|
||||
}
|
||||
|
||||
@customElement("ha-locations-editor")
|
||||
@@ -208,7 +209,7 @@ export class HaLocationsEditor extends LitElement {
|
||||
}
|
||||
);
|
||||
circle.addTo(this._leafletMap!);
|
||||
if (location.editable) {
|
||||
if (location.radius_editable || location.location_editable) {
|
||||
// @ts-ignore
|
||||
circle.editing.enable();
|
||||
// @ts-ignore
|
||||
@@ -230,19 +231,25 @@ export class HaLocationsEditor extends LitElement {
|
||||
// @ts-ignore
|
||||
(ev: MouseEvent) => this._markerClicked(ev)
|
||||
);
|
||||
resizeMarker.addEventListener(
|
||||
"dragend",
|
||||
// @ts-ignore
|
||||
(ev: DragEndEvent) => this._updateRadius(ev)
|
||||
);
|
||||
if (location.radius_editable) {
|
||||
resizeMarker.addEventListener(
|
||||
"dragend",
|
||||
// @ts-ignore
|
||||
(ev: DragEndEvent) => this._updateRadius(ev)
|
||||
);
|
||||
} else {
|
||||
resizeMarker.remove();
|
||||
}
|
||||
this._locationMarkers![location.id] = circle;
|
||||
} else {
|
||||
this._circles[location.id] = circle;
|
||||
}
|
||||
}
|
||||
if (!location.radius || !location.editable) {
|
||||
if (
|
||||
!location.radius ||
|
||||
(!location.radius_editable && !location.location_editable)
|
||||
) {
|
||||
const options: MarkerOptions = {
|
||||
draggable: Boolean(location.editable),
|
||||
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 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) {
|
||||
static get template() {
|
||||
@@ -317,7 +317,7 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
|
||||
const item = items[0];
|
||||
const date = data.datasets[item.datasetIndex].data[item.index].x;
|
||||
|
||||
return formatDateTime(date, this.hass.language);
|
||||
return formatDateTimeWithSeconds(date, this.hass.language);
|
||||
};
|
||||
|
||||
const chartOptions = {
|
||||
|
@@ -6,7 +6,7 @@ import LocalizeMixin from "../mixins/localize-mixin";
|
||||
|
||||
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";
|
||||
|
||||
class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
|
||||
@@ -165,8 +165,8 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
|
||||
const formatTooltipLabel = (item, data) => {
|
||||
const values = data.datasets[item.datasetIndex].data[item.index];
|
||||
|
||||
const start = formatDateTime(values[0], this.hass.language);
|
||||
const end = formatDateTime(values[1], this.hass.language);
|
||||
const start = formatDateTimeWithSeconds(values[0], this.hass.language);
|
||||
const end = formatDateTimeWithSeconds(values[1], this.hass.language);
|
||||
const state = values[2];
|
||||
|
||||
return [state, start, end];
|
||||
|
@@ -173,6 +173,12 @@ export type Condition =
|
||||
| DeviceCondition
|
||||
| LogicalCondition;
|
||||
|
||||
export const triggerAutomation = (hass: HomeAssistant, entityId: string) => {
|
||||
hass.callService("automation", "trigger", {
|
||||
entity_id: entityId,
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteAutomation = (hass: HomeAssistant, id: string) =>
|
||||
hass.callApi("DELETE", `config/automation/config/${id}`);
|
||||
|
||||
|
@@ -9,7 +9,7 @@ import { HomeAssistant } from "../types";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
|
||||
interface CacheConfig {
|
||||
export interface CacheConfig {
|
||||
refresh: number;
|
||||
cacheKey: string;
|
||||
hoursToShow: number;
|
||||
|
@@ -41,15 +41,6 @@ export const fetchThumbnailUrl = async (
|
||||
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 (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user