mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-19 18:10:06 +00:00
Compare commits
78 Commits
20180903.0
...
20180927.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f11ca53282 | ||
![]() |
2c25d6cc0a | ||
![]() |
db6ab4d8ec | ||
![]() |
3961eff372 | ||
![]() |
458a7827f9 | ||
![]() |
68d1c77a79 | ||
![]() |
aa97e30d51 | ||
![]() |
9027d7d391 | ||
![]() |
974fd5de0f | ||
![]() |
f9d28fbf83 | ||
![]() |
b944089087 | ||
![]() |
7b6cf28459 | ||
![]() |
be91688efb | ||
![]() |
a5d47231aa | ||
![]() |
01e833a399 | ||
![]() |
c363ba8056 | ||
![]() |
7cec39ba6c | ||
![]() |
3f15cbd2bd | ||
![]() |
3235d33463 | ||
![]() |
140597c7f8 | ||
![]() |
e1407a7d73 | ||
![]() |
03525c010f | ||
![]() |
8dc202af92 | ||
![]() |
369977f8f3 | ||
![]() |
7f8c092dfc | ||
![]() |
d517cad6e6 | ||
![]() |
62a68890d3 | ||
![]() |
3d8a8cc77b | ||
![]() |
55dc35a8fc | ||
![]() |
82e49a5e44 | ||
![]() |
17ac6f96a0 | ||
![]() |
085db3e0a6 | ||
![]() |
348bebc417 | ||
![]() |
15d21cc673 | ||
![]() |
7e0ff14f28 | ||
![]() |
67d09e8b3d | ||
![]() |
ce3b53a920 | ||
![]() |
a32809e14b | ||
![]() |
1d8c515da2 | ||
![]() |
81e0f1a025 | ||
![]() |
c593e2789c | ||
![]() |
650d2d7a47 | ||
![]() |
2665c86683 | ||
![]() |
8b262f3424 | ||
![]() |
5187f3b84f | ||
![]() |
443e083a79 | ||
![]() |
6c262c20ce | ||
![]() |
cfbf2903c1 | ||
![]() |
19b8ff7d9f | ||
![]() |
ec6ffd2115 | ||
![]() |
433b1e2979 | ||
![]() |
bd3d079dfb | ||
![]() |
fe776191b7 | ||
![]() |
c546d8787d | ||
![]() |
a672b84b88 | ||
![]() |
e3a137c675 | ||
![]() |
10aa99abdc | ||
![]() |
34567d451f | ||
![]() |
494e3dc62c | ||
![]() |
0997274f29 | ||
![]() |
76161329b6 | ||
![]() |
8505750958 | ||
![]() |
4077105db1 | ||
![]() |
3f31d83a55 | ||
![]() |
d729e3c567 | ||
![]() |
9af75f9a43 | ||
![]() |
d32d334a2e | ||
![]() |
94006a843c | ||
![]() |
4790590327 | ||
![]() |
7cf7763e21 | ||
![]() |
0d7979a72f | ||
![]() |
300425e698 | ||
![]() |
59010baf89 | ||
![]() |
47fcb122a2 | ||
![]() |
bbb50b1397 | ||
![]() |
ae8724d699 | ||
![]() |
2169f6979d | ||
![]() |
9cc577e9c7 |
@@ -9,6 +9,7 @@
|
||||
"parser": "babel-eslint",
|
||||
"rules": {
|
||||
"import/no-unresolved": 2,
|
||||
"linebreak-style": 0
|
||||
"linebreak-style": 0,
|
||||
"implicit-arrow-linebreak": 0
|
||||
}
|
||||
}
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -21,6 +21,9 @@ lib
|
||||
bin
|
||||
dist
|
||||
|
||||
# vscode
|
||||
.vscode
|
||||
|
||||
# Secrets
|
||||
.lokalise_token
|
||||
yarn-error.log
|
||||
|
28
Dockerfile
28
Dockerfile
@@ -1,25 +1,31 @@
|
||||
FROM node:8.2.1-alpine
|
||||
FROM node:8.9-alpine
|
||||
|
||||
# install yarn
|
||||
ENV PATH /root/.yarn/bin:$PATH
|
||||
|
||||
## Install/force base tools
|
||||
RUN apk update \
|
||||
&& apk add curl bash binutils tar git python3 \
|
||||
&& apk add make g++ curl bash binutils tar git python2 python3 \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& /bin/bash \
|
||||
&& touch ~/.bashrc \
|
||||
&& curl -o- -L https://yarnpkg.com/install.sh | bash
|
||||
&& touch ~/.bashrc
|
||||
|
||||
## Install yarn
|
||||
RUN curl -o- -L https://yarnpkg.com/install.sh | bash
|
||||
|
||||
## Setup the project
|
||||
RUN mkdir -p /frontend
|
||||
|
||||
WORKDIR /frontend
|
||||
|
||||
ENV NODE_ENV production
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
COPY package.json ./
|
||||
RUN yarn
|
||||
|
||||
COPY bower.json ./
|
||||
RUN ./node_modules/.bin/bower install --allow-root
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
CMD [ "/bin/bash", "./script/build_frontend" ]
|
||||
|
||||
COPY script/docker_entrypoint.sh /usr/bin/docker_entrypoint.sh
|
||||
|
||||
RUN chmod +x /usr/bin/docker_entrypoint.sh
|
||||
|
||||
CMD [ "docker_entrypoint.sh" ]
|
||||
|
12
README.md
12
README.md
@@ -16,6 +16,18 @@ This is the repository for the official [Home Assistant](https://home-assistant.
|
||||
- Gallery: `cd gallery && script/develop_gallery`
|
||||
- Hass.io: [Instructions](https://developers.home-assistant.io/docs/en/hassio_hass.html)
|
||||
|
||||
## 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.
|
||||
|
||||
**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
|
||||
|
||||
## License
|
||||
|
||||
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.
|
||||
|
105
gallery/src/demos/demo-hui-media-player-rows.js
Normal file
105
gallery/src/demos/demo-hui-media-player-rows.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import getEntity from '../data/entity.js';
|
||||
import provideHass from '../data/provide_hass.js';
|
||||
import '../components/demo-cards.js';
|
||||
|
||||
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
|
||||
}),
|
||||
];
|
||||
|
||||
const CONFIGS = [
|
||||
{
|
||||
heading: 'Media Players',
|
||||
config: `
|
||||
- type: entities
|
||||
entities:
|
||||
- entity: media_player.bedroom
|
||||
name: Skip, no pause
|
||||
- entity: media_player.family_room
|
||||
name: Paused, music
|
||||
- entity: media_player.family_room_no_play
|
||||
name: Paused, no play
|
||||
- entity: media_player.living_room
|
||||
name: Pause, No skip, tvshow
|
||||
- entity: media_player.android_cast
|
||||
name: Screen casting
|
||||
- entity: media_player.lounge_room
|
||||
name: Chromcast Idle
|
||||
- entity: media_player.theater
|
||||
name: 'Player Off'
|
||||
`
|
||||
}
|
||||
];
|
||||
|
||||
class DemoHuiMediaPlayerRows 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,
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('demo-hui-media-player-rows', DemoHuiMediaPlayerRows);
|
@@ -43,11 +43,11 @@ class HassioAddonStore extends PolymerElement {
|
||||
sortRepos(a, b) {
|
||||
if (a.slug === 'local') {
|
||||
return -1;
|
||||
} else if (b.slug === 'local') {
|
||||
} if (b.slug === 'local') {
|
||||
return 1;
|
||||
} else if (a.slug === 'core') {
|
||||
} if (a.slug === 'core') {
|
||||
return -1;
|
||||
} else if (b.slug === 'core') {
|
||||
} if (b.slug === 'core') {
|
||||
return 1;
|
||||
}
|
||||
return a.name < b.name ? -1 : 1;
|
||||
|
@@ -195,8 +195,8 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
openChangelog() {
|
||||
this.hass.callApi('get', `hassio/addons/${this.addonSlug}/changelog`)
|
||||
.then(
|
||||
resp => resp
|
||||
, () => 'Error getting changelog'
|
||||
resp => resp,
|
||||
() => 'Error getting changelog'
|
||||
).then((content) => {
|
||||
this.fire('hassio-markdown-dialog', {
|
||||
title: 'Changelog',
|
||||
|
@@ -30,6 +30,7 @@ class HassioHassUpdate extends PolymerElement {
|
||||
<template is="dom-if" if="[[error]]">
|
||||
<div class="error">Error: [[error]]</div>
|
||||
</template>
|
||||
<p><a href='https://www.home-assistant.io/latest-release-notes/' target='_blank'>Read the release notes</a></p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button hass="[[hass]]" path="hassio/homeassistant/update">Update</ha-call-api-button>
|
||||
|
@@ -2,4 +2,3 @@ window.loadES5Adapter().then(() => {
|
||||
import(/* webpackChunkName: "hassio-icons" */ './resources/hassio-icons.js');
|
||||
import(/* webpackChunkName: "hassio-main" */ './hassio-main.js');
|
||||
});
|
||||
|
||||
|
@@ -8,9 +8,10 @@ import './hassio-data.js';
|
||||
import './hassio-pages-with-tabs.js';
|
||||
|
||||
import applyThemesOnElement from '../../src/common/dom/apply_themes_on_element.js';
|
||||
import EventsMixin from '../../src/mixins/events-mixin.js';
|
||||
import NavigateMixin from '../../src/mixins/navigate-mixin.js';
|
||||
|
||||
class HassioMain extends NavigateMixin(PolymerElement) {
|
||||
class HassioMain extends EventsMixin(NavigateMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<app-route route="[[route]]" pattern="/:page" data="{{routeData}}"></app-route>
|
||||
@@ -84,15 +85,16 @@ class HassioMain extends NavigateMixin(PolymerElement) {
|
||||
}
|
||||
|
||||
computeIsLoaded(supervisorInfo, hostInfo, hassInfo) {
|
||||
return (supervisorInfo !== null &&
|
||||
hostInfo !== null &&
|
||||
hassInfo !== null);
|
||||
return (supervisorInfo !== null
|
||||
&& hostInfo !== null
|
||||
&& hassInfo !== null);
|
||||
}
|
||||
|
||||
routeChanged(route) {
|
||||
if (route.path === '' && route.prefix === '/hassio') {
|
||||
this.navigate('/hassio/dashboard', true);
|
||||
}
|
||||
this.fire('iron-resize');
|
||||
}
|
||||
|
||||
equalsAddon(page) {
|
||||
|
@@ -166,8 +166,9 @@ class HassioSnapshot extends PolymerElement {
|
||||
return;
|
||||
}
|
||||
const addons = this.snapshot.addons.filter(addon => addon.checked).map(addon => addon.slug);
|
||||
const folders =
|
||||
this.snapshot.folders.filter(folder => folder.checked).map(folder => folder.slug);
|
||||
const folders = this.snapshot.folders.filter(
|
||||
folder => folder.checked
|
||||
).map(folder => folder.slug);
|
||||
|
||||
const data = {
|
||||
homeassistant: this.restoreHass,
|
||||
|
@@ -17,6 +17,7 @@ class HassioHostInfo extends EventsMixin(PolymerElement) {
|
||||
}
|
||||
.card-content {
|
||||
height: 200px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
@media screen and (max-width: 830px) {
|
||||
paper-card {
|
||||
@@ -145,8 +146,8 @@ class HassioHostInfo extends EventsMixin(PolymerElement) {
|
||||
_showHardware() {
|
||||
this.hass.callApi('get', 'hassio/hardware/info')
|
||||
.then(
|
||||
resp => this._objectToMarkdown(resp.data)
|
||||
, () => 'Error getting hardware info'
|
||||
resp => this._objectToMarkdown(resp.data),
|
||||
() => 'Error getting hardware info'
|
||||
).then((content) => {
|
||||
this.fire('hassio-markdown-dialog', {
|
||||
title: 'Hardware',
|
||||
|
@@ -16,6 +16,7 @@ class HassioSupervisorInfo extends EventsMixin(PolymerElement) {
|
||||
}
|
||||
.card-content {
|
||||
height: 200px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
@media screen and (max-width: 830px) {
|
||||
paper-card {
|
||||
|
172
package.json
172
package.json
@@ -10,96 +10,101 @@
|
||||
"build": "script/build_frontend",
|
||||
"lint": "eslint src hassio/src gallery/src test-mocha && polymer lint",
|
||||
"mocha": "node_modules/.bin/mocha --opts test-mocha/mocha.opts",
|
||||
"test": "npm run lint && npm run mocha"
|
||||
"test": "npm run lint && npm run mocha",
|
||||
"docker_build": "sh ./script/docker_run.sh build $npm_package_version",
|
||||
"bash": "sh ./script/docker_run.sh bash $npm_package_version"
|
||||
},
|
||||
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@mdi/svg": "^2.4.85",
|
||||
"@polymer/app-layout": "^3.0.0-pre.19",
|
||||
"@polymer/app-localize-behavior": "^3.0.0-pre.19",
|
||||
"@polymer/app-route": "^3.0.0-pre.19",
|
||||
"@polymer/app-storage": "^3.0.0-pre.19",
|
||||
"@polymer/font-roboto": "^3.0.0-pre.19",
|
||||
"@polymer/font-roboto-local": "^3.0.0-pre.19",
|
||||
"@polymer/iron-autogrow-textarea": "^3.0.0-pre.19",
|
||||
"@polymer/iron-flex-layout": "^3.0.0-pre.19",
|
||||
"@polymer/iron-icon": "^3.0.0-pre.19",
|
||||
"@polymer/iron-iconset-svg": "^3.0.0-pre.19",
|
||||
"@polymer/iron-image": "^3.0.0-pre.19",
|
||||
"@polymer/iron-input": "^3.0.0-pre.19",
|
||||
"@polymer/iron-label": "^3.0.0-pre.19",
|
||||
"@polymer/iron-media-query": "^3.0.0-pre.19",
|
||||
"@polymer/iron-pages": "^3.0.0-pre.19",
|
||||
"@polymer/iron-resizable-behavior": "^3.0.0-pre.19",
|
||||
"@polymer/neon-animation": "^3.0.0-pre.19",
|
||||
"@polymer/paper-button": "^3.0.0-pre.19",
|
||||
"@polymer/paper-card": "^3.0.0-pre.19",
|
||||
"@polymer/paper-checkbox": "^3.0.0-pre.19",
|
||||
"@polymer/paper-dialog": "^3.0.0-pre.19",
|
||||
"@polymer/paper-dialog-behavior": "^3.0.0-pre.19",
|
||||
"@polymer/paper-dialog-scrollable": "^3.0.0-pre.19",
|
||||
"@polymer/paper-drawer-panel": "^3.0.0-pre.19",
|
||||
"@polymer/paper-dropdown-menu": "^3.0.0-pre.19",
|
||||
"@polymer/paper-fab": "^3.0.0-pre.19",
|
||||
"@polymer/paper-icon-button": "^3.0.0-pre.19",
|
||||
"@polymer/paper-input": "^3.0.0-pre.19",
|
||||
"@polymer/paper-item": "^3.0.0-pre.19",
|
||||
"@polymer/paper-listbox": "^3.0.0-pre.19",
|
||||
"@polymer/paper-menu-button": "^3.0.0-pre.19",
|
||||
"@polymer/paper-progress": "^3.0.0-pre.19",
|
||||
"@polymer/paper-radio-button": "^3.0.0-pre.19",
|
||||
"@polymer/paper-radio-group": "^3.0.0-pre.19",
|
||||
"@polymer/paper-ripple": "^3.0.0-pre.19",
|
||||
"@polymer/paper-scroll-header-panel": "^3.0.0-pre.19",
|
||||
"@polymer/paper-slider": "^3.0.0-pre.19",
|
||||
"@polymer/paper-spinner": "^3.0.0-pre.19",
|
||||
"@polymer/paper-styles": "^3.0.0-pre.19",
|
||||
"@polymer/paper-tabs": "^3.0.0-pre.19",
|
||||
"@polymer/paper-toast": "^3.0.0-pre.19",
|
||||
"@polymer/paper-toggle-button": "^3.0.0-pre.19",
|
||||
"@polymer/polymer": "^3.0.2",
|
||||
"@vaadin/vaadin-combo-box": "4.1.0-alpha2",
|
||||
"@vaadin/vaadin-date-picker": "3.2.0-alpha3",
|
||||
"@webcomponents/shadycss": "^1.3.1",
|
||||
"@webcomponents/webcomponentsjs": "^2.0.2",
|
||||
"@mdi/svg": "^2.7.94",
|
||||
"@polymer/app-layout": "^3.0.1",
|
||||
"@polymer/app-localize-behavior": "^3.0.1",
|
||||
"@polymer/app-route": "^3.0.2",
|
||||
"@polymer/app-storage": "^3.0.2",
|
||||
"@polymer/font-roboto": "^3.0.2",
|
||||
"@polymer/font-roboto-local": "^3.0.2",
|
||||
"@polymer/iron-autogrow-textarea": "^3.0.1",
|
||||
"@polymer/iron-flex-layout": "^3.0.1",
|
||||
"@polymer/iron-icon": "^3.0.1",
|
||||
"@polymer/iron-iconset-svg": "^3.0.1",
|
||||
"@polymer/iron-image": "^3.0.1",
|
||||
"@polymer/iron-input": "^3.0.1",
|
||||
"@polymer/iron-label": "^3.0.1",
|
||||
"@polymer/iron-media-query": "^3.0.1",
|
||||
"@polymer/iron-pages": "^3.0.1",
|
||||
"@polymer/iron-resizable-behavior": "^3.0.1",
|
||||
"@polymer/neon-animation": "^3.0.1",
|
||||
"@polymer/paper-button": "^3.0.1",
|
||||
"@polymer/paper-card": "^3.0.1",
|
||||
"@polymer/paper-checkbox": "^3.0.1",
|
||||
"@polymer/paper-dialog": "^3.0.1",
|
||||
"@polymer/paper-dialog-behavior": "^3.0.1",
|
||||
"@polymer/paper-dialog-scrollable": "^3.0.1",
|
||||
"@polymer/paper-drawer-panel": "^3.0.1",
|
||||
"@polymer/paper-dropdown-menu": "^3.0.1",
|
||||
"@polymer/paper-fab": "^3.0.1",
|
||||
"@polymer/paper-icon-button": "^3.0.1",
|
||||
"@polymer/paper-input": "^3.0.1",
|
||||
"@polymer/paper-item": "^3.0.1",
|
||||
"@polymer/paper-listbox": "^3.0.1",
|
||||
"@polymer/paper-menu-button": "^3.0.1",
|
||||
"@polymer/paper-progress": "^3.0.1",
|
||||
"@polymer/paper-radio-button": "^3.0.1",
|
||||
"@polymer/paper-radio-group": "^3.0.1",
|
||||
"@polymer/paper-ripple": "^3.0.1",
|
||||
"@polymer/paper-scroll-header-panel": "^3.0.1",
|
||||
"@polymer/paper-slider": "^3.0.1",
|
||||
"@polymer/paper-spinner": "^3.0.1",
|
||||
"@polymer/paper-styles": "^3.0.1",
|
||||
"@polymer/paper-tabs": "^3.0.1",
|
||||
"@polymer/paper-toast": "^3.0.1",
|
||||
"@polymer/paper-toggle-button": "^3.0.1",
|
||||
"@polymer/paper-tooltip": "^3.0.1",
|
||||
"@polymer/polymer": "^3.0.5",
|
||||
"@vaadin/vaadin-combo-box": "4.2.0-alpha3",
|
||||
"@vaadin/vaadin-date-picker": "3.3.0-alpha1",
|
||||
"@webcomponents/shadycss": "^1.5.2",
|
||||
"@webcomponents/webcomponentsjs": "^2.1.3",
|
||||
"chart.js": "~2.7.2",
|
||||
"chartjs-chart-timeline": "^0.2.1",
|
||||
"es6-object-assign": "^1.1.0",
|
||||
"eslint-import-resolver-webpack": "^0.10.0",
|
||||
"eslint-import-resolver-webpack": "^0.10.1",
|
||||
"fecha": "^2.3.3",
|
||||
"home-assistant-js-websocket": "^3.0.0",
|
||||
"home-assistant-js-websocket": "^3.1.4",
|
||||
"intl-messageformat": "^2.2.0",
|
||||
"js-yaml": "^3.12.0",
|
||||
"leaflet": "^1.3.1",
|
||||
"marked": "^0.4.0",
|
||||
"mdn-polyfills": "^5.8.0",
|
||||
"leaflet": "^1.3.4",
|
||||
"marked": "^0.5.0",
|
||||
"mdn-polyfills": "^5.12.0",
|
||||
"moment": "^2.22.2",
|
||||
"preact": "^8.2.9",
|
||||
"preact-compat": "^3.18.0",
|
||||
"react-big-calendar": "^0.19.1",
|
||||
"regenerator-runtime": "^0.11.1",
|
||||
"unfetch": "^3.0.0",
|
||||
"preact": "^8.3.1",
|
||||
"preact-compat": "^3.18.4",
|
||||
"react-big-calendar": "^0.19.2",
|
||||
"regenerator-runtime": "^0.12.1",
|
||||
"unfetch": "^4.0.1",
|
||||
"web-animations-js": "^2.3.1",
|
||||
"xss": "^1.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gfx/zopfli": "^1.0.9",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-eslint": "^8.2.3",
|
||||
"babel-eslint": "^9.0.0",
|
||||
"babel-loader": "^7.1.4",
|
||||
"babel-minify-webpack-plugin": "^0.3.1",
|
||||
"babel-plugin-external-helpers": "^6.22.0",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-plugin-transform-react-jsx": "^6.24.1",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"chai": "^4.1.2",
|
||||
"compression-webpack-plugin": "^1.1.11",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
"compression-webpack-plugin": "^2.0.0",
|
||||
"copy-webpack-plugin": "^4.5.2",
|
||||
"del": "^3.0.0",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-config-airbnb-base": "^12.1.0",
|
||||
"eslint-plugin-import": "^2.12.0",
|
||||
"eslint-plugin-react": "^7.9.1",
|
||||
"eslint": "^5.6.0",
|
||||
"eslint-config-airbnb-base": "^13.1.0",
|
||||
"eslint-plugin-import": "^2.14.0",
|
||||
"eslint-plugin-react": "^7.11.1",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-foreach": "^0.1.0",
|
||||
"gulp-hash": "^4.2.2",
|
||||
@@ -107,36 +112,37 @@
|
||||
"gulp-json-transform": "^0.4.5",
|
||||
"gulp-jsonminify": "^1.1.0",
|
||||
"gulp-merge-json": "^1.3.1",
|
||||
"gulp-rename": "^1.3.0",
|
||||
"gulp-rename": "^1.4.0",
|
||||
"html-loader": "^0.5.5",
|
||||
"html-minifier": "^3.5.16",
|
||||
"html-minifier": "^3.5.20",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"merge-stream": "^1.0.1",
|
||||
"mocha": "^5.2.0",
|
||||
"parse5": "^5.0.0",
|
||||
"polymer-analyzer": "^3.0.1",
|
||||
"polymer-bundler": "^4.0.1",
|
||||
"polymer-cli": "^1.7.4",
|
||||
"parse5": "^5.1.0",
|
||||
"polymer-analyzer": "^3.1.2",
|
||||
"polymer-bundler": "^4.0.2",
|
||||
"polymer-cli": "^1.8.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"reify": "^0.16.2",
|
||||
"reify": "^0.17.3",
|
||||
"require-dir": "^1.0.0",
|
||||
"sinon": "^6.0.0",
|
||||
"uglifyjs-webpack-plugin": "^1.2.6",
|
||||
"sinon": "^6.3.4",
|
||||
"wct-browser-legacy": "^1.0.1",
|
||||
"web-component-tester": "^6.7.0",
|
||||
"webpack": "^4.12.0",
|
||||
"webpack-cli": "^3.0.8",
|
||||
"webpack-dev-server": "^3.1.4",
|
||||
"workbox-webpack-plugin": "^3.3.0"
|
||||
"web-component-tester": "^6.8.0",
|
||||
"webpack": "^4.19.1",
|
||||
"webpack-cli": "^3.1.0",
|
||||
"webpack-dev-server": "^3.1.8",
|
||||
"workbox-webpack-plugin": "^3.5.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"inherits": "2.0.3",
|
||||
"samsam": "1.1.3",
|
||||
"supports-color": "3.1.2",
|
||||
"type-detect": "1.0.0",
|
||||
"@webcomponents/webcomponentsjs": "2.0.2",
|
||||
"@webcomponents/shadycss": "^1.3.1",
|
||||
"@vaadin/vaadin-overlay": "3.0.2-pre.2",
|
||||
"@polymer/polymer": "3.0.5",
|
||||
"@webcomponents/webcomponentsjs": "2.1.3",
|
||||
"@webcomponents/shadycss": "^1.5.2",
|
||||
"@vaadin/vaadin-overlay": "3.2.0-alpha3",
|
||||
"@vaadin/vaadin-lumo-styles": "1.2.0",
|
||||
"fecha": "https://github.com/balloob/fecha/archive/51d14fd0eb4781e2ecf265d1c3080706259133b5.tar.gz"
|
||||
},
|
||||
"main": "src/home-assistant.js"
|
||||
|
@@ -24,7 +24,7 @@
|
||||
],
|
||||
"lint": {
|
||||
"rules": ["polymer-3"],
|
||||
"ignoreWarnings": ["could-not-resolve-reference"],
|
||||
"ignoreWarnings": ["could-not-resolve-reference", "could-not-load"],
|
||||
"filesToIgnore": [
|
||||
"**/*.html",
|
||||
"**/src/panels/config/js/**/*.js",
|
||||
|
14
script/docker_entrypoint.sh
Normal file
14
script/docker_entrypoint.sh
Normal file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# Docker entry point inspired by travis build and script/build_frontend
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
# Build the frontend but not used the npm run build
|
||||
/bin/bash script/build_frontend
|
||||
|
||||
# TEST
|
||||
npm run test
|
||||
|
||||
#
|
||||
#xvfb-run wct
|
103
script/docker_run.sh
Executable file
103
script/docker_run.sh
Executable file
@@ -0,0 +1,103 @@
|
||||
#!/bin/bash
|
||||
# Basic Docker Management scripts
|
||||
# With this script you can build software, or enter an agnostic development environment and run commands interactively.
|
||||
|
||||
|
||||
|
||||
check_mandatory_tools(){
|
||||
if [ "x$(which docker)" == "x" ]; then
|
||||
echo "UNKNOWN - Missing docker binary! Are you sure it is installed and reachable?"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
docker info > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "UNKNOWN - Unable to talk to the docker daemon! Maybe the docker daemon is not running"
|
||||
exit 3
|
||||
fi
|
||||
}
|
||||
|
||||
check_dev_image(){
|
||||
if [[ "$(docker images -q ${IMAGE_NAME}:$IMAGE_TAG 2> /dev/null)" == "" ]]; then
|
||||
echo "UNKNOWN - Can't find the development docker image ${IMAGE_NAME}:$IMAGE_TAG"
|
||||
while true; do
|
||||
read -p "Do you want to create it now?" yn
|
||||
case $yn in
|
||||
[Yy]* ) create_image; break;;
|
||||
[Nn]* ) exit 3;;
|
||||
* ) echo "Please answer y or n";;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
# Building the basic image for compiling the production frontend
|
||||
create_image(){
|
||||
docker build -t ${IMAGE_NAME}:${IMAGE_TAG} .
|
||||
}
|
||||
|
||||
#
|
||||
# Execute interactive bash on basic image
|
||||
#
|
||||
run_bash_on_docker(){
|
||||
|
||||
check_dev_image
|
||||
|
||||
docker run -it \
|
||||
-v $PWD/:/frontend/ \
|
||||
-v /frontend/node_modules \
|
||||
-v /frontend/bower_components \
|
||||
${IMAGE_NAME}:${IMAGE_TAG} /bin/bash
|
||||
}
|
||||
|
||||
#
|
||||
# Execute the basic image for compiling the production frontend
|
||||
#
|
||||
build_all(){
|
||||
|
||||
check_dev_image
|
||||
|
||||
docker run -it \
|
||||
-v $PWD/:/frontend/ \
|
||||
-v /frontend/node_modules \
|
||||
-v /frontend/bower_components \
|
||||
${IMAGE_NAME}:${IMAGE_TAG} /bin/bash script/build_frontend
|
||||
|
||||
}
|
||||
|
||||
# Init Global Variable
|
||||
IMAGE_NAME=home_assistant_fe_image
|
||||
IMAGE_TAG=${2:-latest}
|
||||
|
||||
check_mandatory_tools
|
||||
|
||||
case "$1" in
|
||||
setup_env)
|
||||
create_image
|
||||
;;
|
||||
bash)
|
||||
run_bash_on_docker
|
||||
;;
|
||||
build)
|
||||
build_all
|
||||
;;
|
||||
*)
|
||||
echo "NAME"
|
||||
echo " Docker Management."
|
||||
echo ""
|
||||
echo "SYNOPSIS"
|
||||
echo " ${0} command [version]"
|
||||
echo ""
|
||||
echo "DESCRIPTION"
|
||||
echo " With this script you can build software, or enter an agnostic development environment and run commands interactively."
|
||||
echo ""
|
||||
echo " The command are:"
|
||||
echo " setup_env Create develop images"
|
||||
echo " bash Run bash on develop enviroments"
|
||||
echo " build Run silent build"
|
||||
echo ""
|
||||
echo " The version is optional, if not inserted it assumes \"latest\". "
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
exit 0
|
2
setup.py
2
setup.py
@@ -1,7 +1,7 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(name='home-assistant-frontend',
|
||||
version='20180903.0',
|
||||
version='20180927.0',
|
||||
description='The Home Assistant frontend',
|
||||
url='https://github.com/home-assistant/home-assistant-polymer',
|
||||
author='The Home Assistant Authors',
|
||||
|
@@ -137,8 +137,8 @@ class HaAuthFlow extends LocalizeLiteMixin(PolymerElement) {
|
||||
_state: 'step',
|
||||
};
|
||||
|
||||
if (this._step &&
|
||||
(step.flow_id !== this._step.flow_id || step.step_id !== this._step.step_id)) {
|
||||
if (this._step
|
||||
&& (step.flow_id !== this._step.flow_id || step.step_id !== this._step.step_id)) {
|
||||
props._stepData = {};
|
||||
}
|
||||
|
||||
|
@@ -7,7 +7,7 @@ import '../components/ha-markdown.js';
|
||||
|
||||
import LocalizeLiteMixin from '../mixins/localize-lite-mixin.js';
|
||||
|
||||
import '../auth/ha-auth-flow.js';
|
||||
import './ha-auth-flow.js';
|
||||
|
||||
class HaAuthorize extends LocalizeLiteMixin(PolymerElement) {
|
||||
static get template() {
|
||||
@@ -94,6 +94,12 @@ class HaAuthorize extends LocalizeLiteMixin(PolymerElement) {
|
||||
const response = await window.providersPromise;
|
||||
const authProviders = await response.json();
|
||||
|
||||
// Forward to main screen which will redirect to right onboarding page.
|
||||
if (response.status === 400 && authProviders.code === 'onboarding_required') {
|
||||
location.href = '/';
|
||||
return;
|
||||
}
|
||||
|
||||
if (authProviders.length === 0) {
|
||||
alert('No auth providers returned. Unable to finish login.');
|
||||
return;
|
||||
@@ -119,15 +125,14 @@ class HaAuthorize extends LocalizeLiteMixin(PolymerElement) {
|
||||
}
|
||||
|
||||
_computeInactiveProvders(curProvider, providers) {
|
||||
return providers.filter(prv =>
|
||||
prv.type !== curProvider.type || prv.id !== curProvider.id);
|
||||
return providers.filter(prv => prv.type !== curProvider.type || prv.id !== curProvider.id);
|
||||
}
|
||||
|
||||
_computeIntro(localize, clientId, authProvider) {
|
||||
return (
|
||||
localize('ui.panel.page-authorize.authorizing_client', 'clientId', clientId) +
|
||||
'\n\n' +
|
||||
localize('ui.panel.page-authorize.logging_in_with', 'authProviderName', authProvider.name)
|
||||
localize('ui.panel.page-authorize.authorizing_client', 'clientId', clientId)
|
||||
+ '\n\n'
|
||||
+ localize('ui.panel.page-authorize.logging_in_with', 'authProviderName', authProvider.name)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -58,8 +58,7 @@ class HaCardChooser extends PolymerElement {
|
||||
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');
|
||||
const eligibleToObserver = (window.IntersectionObserver && newData.cardType !== 'entities');
|
||||
if (!eligibleToObserver) {
|
||||
if (this.observer) {
|
||||
this.observer.unobserve(this);
|
||||
|
@@ -134,8 +134,8 @@ class HaEntitiesCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
}
|
||||
|
||||
showGroupToggle(groupEntity, states) {
|
||||
if (!groupEntity || !states || groupEntity.attributes.control === 'hidden' ||
|
||||
(groupEntity.state !== 'on' && groupEntity.state !== 'off')) {
|
||||
if (!groupEntity || !states || groupEntity.attributes.control === 'hidden'
|
||||
|| (groupEntity.state !== 'on' && groupEntity.state !== 'off')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@@ -76,9 +76,9 @@ class HaHistoryGraphCard extends EventsMixin(PolymerElement) {
|
||||
|
||||
stateObjObserver(stateObj) {
|
||||
if (!stateObj) return;
|
||||
if (this.cacheConfig.cacheKey !== stateObj.entity_id ||
|
||||
this.cacheConfig.refresh !== (stateObj.attributes.refresh || 0) ||
|
||||
this.cacheConfig.hoursToShow !== (stateObj.attributes.hours_to_show || 24)) {
|
||||
if (this.cacheConfig.cacheKey !== stateObj.entity_id
|
||||
|| this.cacheConfig.refresh !== (stateObj.attributes.refresh || 0)
|
||||
|| this.cacheConfig.hoursToShow !== (stateObj.attributes.hours_to_show || 24)) {
|
||||
this.cacheConfig = Object.assign({}, {
|
||||
refresh: stateObj.attributes.refresh || 0,
|
||||
cacheKey: stateObj.entity_id,
|
||||
|
@@ -219,7 +219,7 @@ class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
if (picture !== oldPicture && !picture) {
|
||||
this.$.cover.style.backgroundImage = '';
|
||||
return;
|
||||
} else if (picture === oldPicture) {
|
||||
} if (picture === oldPicture) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -275,7 +275,7 @@ class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
computePlaybackControlIcon(playerObj) {
|
||||
if (playerObj.isPlaying) {
|
||||
return playerObj.supportsPause ? 'hass:pause' : 'hass:stop';
|
||||
} else if (playerObj.hasMediaControl || playerObj.isOff || playerObj.isIdle) {
|
||||
} if (playerObj.hasMediaControl || playerObj.isOff || playerObj.isIdle) {
|
||||
if (playerObj.hasMediaControl && playerObj.supportsPause && !playerObj.isPaused) {
|
||||
return 'hass:play-pause';
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import '../components/ha-markdown.js';
|
||||
|
||||
import computeStateName from '../common/entity/compute_state_name.js';
|
||||
import LocalizeMixin from '../mixins/localize-mixin.js';
|
||||
import computeObjectId from '../common/entity/compute_object_id';
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
@@ -59,13 +60,15 @@ class HaPersistentNotificationCard extends LocalizeMixin(PolymerElement) {
|
||||
}
|
||||
|
||||
computeTitle(stateObj) {
|
||||
return (stateObj.attributes.title ||
|
||||
computeStateName(stateObj));
|
||||
return (stateObj.attributes.title
|
||||
|| computeStateName(stateObj));
|
||||
}
|
||||
|
||||
dismissTap(ev) {
|
||||
ev.preventDefault();
|
||||
this.hass.callApi('DELETE', 'states/' + this.stateObj.entity_id);
|
||||
this.hass.callService('persistent_notification', 'dismiss', {
|
||||
notification_id: computeObjectId(this.stateObj.entity_id)
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define('ha-persistent_notification-card', HaPersistentNotificationCard);
|
||||
|
@@ -113,7 +113,7 @@ class HaPlantCard extends EventsMixin(PolymerElement) {
|
||||
if (attr === 'battery') {
|
||||
if (batLvl <= 5) {
|
||||
return `${icon}-alert`;
|
||||
} else if (batLvl < 95) {
|
||||
} if (batLvl < 95) {
|
||||
return `${icon}-${Math.round((batLvl / 10) - 0.01) * 10}`;
|
||||
}
|
||||
}
|
||||
|
@@ -176,7 +176,7 @@ class HaWeatherCard extends
|
||||
cloudy: 'hass:weather-cloudy',
|
||||
fog: 'hass:weather-fog',
|
||||
hail: 'hass:weather-hail',
|
||||
lightning: 'mid:weather-lightning',
|
||||
lightning: 'hass:weather-lightning',
|
||||
'lightning-rainy': 'hass:weather-lightning-rainy',
|
||||
partlycloudy: 'hass:weather-partlycloudy',
|
||||
pouring: 'hass:weather-pouring',
|
||||
|
70
src/common/auth/external_auth.js
Normal file
70
src/common/auth/external_auth.js
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Auth class that connects to a native app for authentication.
|
||||
*/
|
||||
import { Auth } from 'home-assistant-js-websocket';
|
||||
|
||||
const CALLBACK_SET_TOKEN = 'externalAuthSetToken';
|
||||
const CALLBACK_REVOKE_TOKEN = 'externalAuthRevokeToken';
|
||||
|
||||
if (!window.externalApp && !window.webkit) {
|
||||
throw new Error('External auth requires either externalApp or webkit defined on Window object.');
|
||||
}
|
||||
|
||||
export default class ExternalAuth extends Auth {
|
||||
constructor(hassUrl) {
|
||||
super();
|
||||
|
||||
this.data = {
|
||||
hassUrl,
|
||||
access_token: '',
|
||||
// This will trigger connection to do a refresh right away
|
||||
expires: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async refreshAccessToken() {
|
||||
const responseProm = new Promise((resolve, reject) => {
|
||||
window[CALLBACK_SET_TOKEN] = (success, data) => (success ? resolve(data) : reject(data));
|
||||
});
|
||||
|
||||
// Allow promise to set resolve on window object.
|
||||
await 0;
|
||||
|
||||
const callbackPayload = { callback: CALLBACK_SET_TOKEN };
|
||||
|
||||
if (window.externalApp) {
|
||||
window.externalApp.getExternalAuth(callbackPayload);
|
||||
} else {
|
||||
window.webkit.messageHandlers.getExternalAuth.postMessage(callbackPayload);
|
||||
}
|
||||
|
||||
// Response we expect back:
|
||||
// {
|
||||
// "access_token": "qwere",
|
||||
// "expires_in": 1800
|
||||
// }
|
||||
const tokens = await responseProm;
|
||||
|
||||
this.data.access_token = tokens.access_token;
|
||||
this.data.expires = (tokens.expires_in * 1000) + Date.now();
|
||||
}
|
||||
|
||||
async revoke() {
|
||||
const responseProm = new Promise((resolve, reject) => {
|
||||
window[CALLBACK_REVOKE_TOKEN] = (success, data) => (success ? resolve(data) : reject(data));
|
||||
});
|
||||
|
||||
// Allow promise to set resolve on window object.
|
||||
await 0;
|
||||
|
||||
const callbackPayload = { callback: CALLBACK_REVOKE_TOKEN };
|
||||
|
||||
if (window.externalApp) {
|
||||
window.externalApp.revokeExternalAuth(callbackPayload);
|
||||
} else {
|
||||
window.webkit.messageHandlers.revokeExternalAuth.postMessage(callbackPayload);
|
||||
}
|
||||
|
||||
await responseProm;
|
||||
}
|
||||
}
|
@@ -10,8 +10,8 @@ function toLocaleDateStringSupportsOptions() {
|
||||
return false;
|
||||
}
|
||||
|
||||
export default (toLocaleDateStringSupportsOptions() ?
|
||||
function (dateObj, locales) {
|
||||
export default (toLocaleDateStringSupportsOptions()
|
||||
? function (dateObj, locales) {
|
||||
return dateObj.toLocaleDateString(
|
||||
locales,
|
||||
{ year: 'numeric', month: 'long', day: 'numeric' },
|
||||
|
@@ -10,8 +10,8 @@ function toLocaleStringSupportsOptions() {
|
||||
return false;
|
||||
}
|
||||
|
||||
export default (toLocaleStringSupportsOptions() ?
|
||||
function (dateObj, locales) {
|
||||
export default (toLocaleStringSupportsOptions()
|
||||
? function (dateObj, locales) {
|
||||
return dateObj.toLocaleString(locales, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
|
@@ -10,8 +10,8 @@ function toLocaleTimeStringSupportsOptions() {
|
||||
return false;
|
||||
}
|
||||
|
||||
export default (toLocaleTimeStringSupportsOptions() ?
|
||||
function (dateObj, locales) {
|
||||
export default (toLocaleTimeStringSupportsOptions()
|
||||
? function (dateObj, locales) {
|
||||
return dateObj.toLocaleTimeString(
|
||||
locales,
|
||||
{ hour: 'numeric', minute: '2-digit' }
|
||||
|
@@ -7,9 +7,9 @@ export default function secondsToDuration(d) {
|
||||
|
||||
if (h > 0) {
|
||||
return `${h}:${leftPad(m)}:${leftPad(s)}`;
|
||||
} else if (m > 0) {
|
||||
} if (m > 0) {
|
||||
return `${m}:${leftPad(s)}`;
|
||||
} else if (s > 0) {
|
||||
} if (s > 0) {
|
||||
return '' + s;
|
||||
}
|
||||
return null;
|
||||
|
@@ -4,7 +4,7 @@ export default function canToggleDomain(hass, domain) {
|
||||
|
||||
if (domain === 'lock') {
|
||||
return 'lock' in services;
|
||||
} else if (domain === 'cover') {
|
||||
} if (domain === 'cover') {
|
||||
return 'open_cover' in services;
|
||||
}
|
||||
return 'turn_on' in services;
|
||||
|
@@ -9,8 +9,7 @@ export default function computeStateDisplay(localize, stateObj, language) {
|
||||
if (domain === 'binary_sensor') {
|
||||
// Try device class translation, then default binary sensor translation
|
||||
if (stateObj.attributes.device_class) {
|
||||
stateObj._stateDisplay =
|
||||
localize(`state.${domain}.${stateObj.attributes.device_class}.${stateObj.state}`);
|
||||
stateObj._stateDisplay = localize(`state.${domain}.${stateObj.attributes.device_class}.${stateObj.state}`);
|
||||
}
|
||||
if (!stateObj._stateDisplay) {
|
||||
stateObj._stateDisplay = localize(`state.${domain}.default.${stateObj.state}`);
|
||||
|
@@ -3,8 +3,8 @@ import computeObjectId from './compute_object_id';
|
||||
export default function computeStateName(stateObj) {
|
||||
if (stateObj._entityDisplay === undefined) {
|
||||
stateObj._entityDisplay = (
|
||||
stateObj.attributes.friendly_name ||
|
||||
computeObjectId(stateObj.entity_id).replace(/_/g, ' '));
|
||||
stateObj.attributes.friendly_name
|
||||
|| computeObjectId(stateObj.entity_id).replace(/_/g, ' '));
|
||||
}
|
||||
|
||||
return stateObj._entityDisplay;
|
||||
|
@@ -6,6 +6,7 @@
|
||||
import { DEFAULT_DOMAIN_ICON } from '../const.js';
|
||||
|
||||
const fixedIcons = {
|
||||
alert: 'hass:alert',
|
||||
automation: 'hass:playlist-play',
|
||||
calendar: 'hass:calendar',
|
||||
camera: 'hass:video',
|
||||
@@ -71,8 +72,8 @@ export default function domainIcon(domain, state) {
|
||||
return state && state === 'unlocked' ? 'hass:lock-open' : 'hass:lock';
|
||||
|
||||
case 'media_player':
|
||||
return state && state !== 'off' && state !== 'idle' ?
|
||||
'hass:cast-connected' : 'hass:cast';
|
||||
return state && state !== 'off' && state !== 'idle'
|
||||
? 'hass:cast-connected' : 'hass:cast';
|
||||
|
||||
case 'zwave':
|
||||
switch (state) {
|
||||
@@ -83,7 +84,7 @@ export default function domainIcon(domain, state) {
|
||||
case 'initializing':
|
||||
return 'hass:timer-sand';
|
||||
default:
|
||||
return 'hass:nfc';
|
||||
return 'hass:z-wave';
|
||||
}
|
||||
|
||||
default:
|
||||
|
@@ -14,7 +14,7 @@ export default function extractViews(entities) {
|
||||
views.sort((view1, view2) => {
|
||||
if (view1.entity_id === DEFAULT_VIEW_ENTITY_ID) {
|
||||
return -1;
|
||||
} else if (view2.entity_id === DEFAULT_VIEW_ENTITY_ID) {
|
||||
} if (view2.entity_id === DEFAULT_VIEW_ENTITY_ID) {
|
||||
return 1;
|
||||
}
|
||||
return view1.attributes.order - view2.attributes.order;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
export default function hasLocation(stateObj) {
|
||||
return ('latitude' in stateObj.attributes &&
|
||||
'longitude' in stateObj.attributes);
|
||||
return ('latitude' in stateObj.attributes
|
||||
&& 'longitude' in stateObj.attributes);
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ import domainIcon from './domain_icon.js';
|
||||
export default function inputDateTimeIcon(state) {
|
||||
if (!state.attributes.has_date) {
|
||||
return 'hass:clock';
|
||||
} else if (!state.attributes.has_time) {
|
||||
} if (!state.attributes.has_time) {
|
||||
return 'hass:calendar';
|
||||
}
|
||||
return domainIcon('input_datetime');
|
||||
|
@@ -13,7 +13,7 @@ export default function sensorIcon(state) {
|
||||
|
||||
if (dclass in fixedDeviceClassIcons) {
|
||||
return fixedDeviceClassIcons[dclass];
|
||||
} else if (dclass === 'battery') {
|
||||
} if (dclass === 'battery') {
|
||||
if (isNaN(state.state)) {
|
||||
return 'hass:battery-unknown';
|
||||
}
|
||||
|
@@ -17,8 +17,9 @@ export default function splitByGroups(entities) {
|
||||
}
|
||||
});
|
||||
|
||||
groups.forEach(group =>
|
||||
group.attributes.entity_id.forEach((entityId) => { delete ungrouped[entityId]; }));
|
||||
groups.forEach(group => group.attributes.entity_id.forEach((entityId) => {
|
||||
delete ungrouped[entityId];
|
||||
}));
|
||||
|
||||
return { groups, ungrouped };
|
||||
}
|
||||
|
@@ -11,8 +11,8 @@ export default function stateCardType(hass, stateObj) {
|
||||
|
||||
if (DOMAINS_WITH_CARD.includes(domain)) {
|
||||
return domain;
|
||||
} else if (canToggleState(hass, stateObj) &&
|
||||
stateObj.attributes.control !== 'hidden') {
|
||||
} if (canToggleState(hass, stateObj)
|
||||
&& stateObj.attributes.control !== 'hidden') {
|
||||
return 'toggle';
|
||||
}
|
||||
return 'display';
|
||||
|
@@ -19,7 +19,7 @@ const domainIcons = {
|
||||
export default function stateIcon(state) {
|
||||
if (!state) {
|
||||
return DEFAULT_DOMAIN_ICON;
|
||||
} else if (state.attributes.icon) {
|
||||
} if (state.attributes.icon) {
|
||||
return state.attributes.icon;
|
||||
}
|
||||
|
||||
|
10
src/common/string/compare.js
Normal file
10
src/common/string/compare.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export default (a, b) => {
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
if (a > b) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
24
src/common/util/parse-aspect-ratio.js
Normal file
24
src/common/util/parse-aspect-ratio.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default function parseAspectRatio(input) {
|
||||
// Handle 16x9, 16:9, 1.78x1, 1.78:1, 1.78
|
||||
// Ignore everything else
|
||||
function parseOrThrow(number) {
|
||||
const parsed = parseFloat(number);
|
||||
if (isNaN(parsed)) throw new Error(`${number} is not a number`);
|
||||
return parsed;
|
||||
}
|
||||
try {
|
||||
if (input) {
|
||||
const arr = input.replace(':', 'x').split('x');
|
||||
if (arr.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return arr.length === 1
|
||||
? { w: parseOrThrow(arr[0]), h: 1 }
|
||||
: { w: parseOrThrow(arr[0]), h: parseOrThrow(arr[1]) };
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore the error
|
||||
}
|
||||
return null;
|
||||
}
|
@@ -219,12 +219,14 @@ class HaChartBase extends mixinBehaviors([
|
||||
this._resizeTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
onPropsChange() {
|
||||
if (!this._isAttached || !this.ChartClass || !this.data) {
|
||||
return;
|
||||
}
|
||||
this.drawChart();
|
||||
}
|
||||
|
||||
_customTooltips(tooltip) {
|
||||
// Hide if no tooltip
|
||||
if (tooltip.opacity === 0) {
|
||||
@@ -285,18 +287,18 @@ class HaChartBase extends mixinBehaviors([
|
||||
this.set(['metas', index, 'hidden'], this._chart.isDatasetVisible(index) ? null : 'hidden');
|
||||
this._chart.update();
|
||||
}
|
||||
|
||||
_drawLegend() {
|
||||
const chart = this._chart;
|
||||
// New data for old graph. Keep metadata.
|
||||
const preserveVisibility =
|
||||
this._oldIdentifier && this.identifier === this._oldIdentifier;
|
||||
const preserveVisibility = this._oldIdentifier && this.identifier === this._oldIdentifier;
|
||||
this._oldIdentifier = this.identifier;
|
||||
this.set('metas', this._chart.data.datasets.map((x, i) => ({
|
||||
label: x.label,
|
||||
color: x.color,
|
||||
bgColor: x.backgroundColor,
|
||||
hidden: preserveVisibility && i < this.metas.length ?
|
||||
this.metas[i].hidden : !chart.isDatasetVisible(i),
|
||||
hidden: preserveVisibility && i < this.metas.length
|
||||
? this.metas[i].hidden : !chart.isDatasetVisible(i),
|
||||
})));
|
||||
let updateNeeded = false;
|
||||
if (preserveVisibility) {
|
||||
@@ -311,6 +313,7 @@ class HaChartBase extends mixinBehaviors([
|
||||
}
|
||||
this.unit = this.data.unit;
|
||||
}
|
||||
|
||||
_formatTickValue(value, index, values) {
|
||||
if (values.length === 0) {
|
||||
return value;
|
||||
@@ -318,6 +321,7 @@ class HaChartBase extends mixinBehaviors([
|
||||
const date = new Date(values[index].value);
|
||||
return formatTime(date);
|
||||
}
|
||||
|
||||
drawChart() {
|
||||
const data = this.data.data;
|
||||
const ctx = this.$.chartCanvas;
|
||||
@@ -493,6 +497,7 @@ class HaChartBase extends mixinBehaviors([
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static getColorGenerator(staticColors, startIndex) {
|
||||
// Known colors for static data,
|
||||
// should add for very common state string manually.
|
||||
|
@@ -133,15 +133,15 @@ class HaStateLabelBadge extends
|
||||
case 'alarm_control_panel':
|
||||
if (state.state === 'pending') {
|
||||
return 'hass:clock-fast';
|
||||
} else if (state.state === 'armed_away') {
|
||||
} if (state.state === 'armed_away') {
|
||||
return 'hass:nature';
|
||||
} else if (state.state === 'armed_home') {
|
||||
} if (state.state === 'armed_home') {
|
||||
return 'hass:home-variant';
|
||||
} else if (state.state === 'armed_night') {
|
||||
} if (state.state === 'armed_night') {
|
||||
return 'hass:weather-night';
|
||||
} else if (state.state === 'armed_custom_bypass') {
|
||||
} if (state.state === 'armed_custom_bypass') {
|
||||
return 'hass:security-home';
|
||||
} else if (state.state === 'triggered') {
|
||||
} if (state.state === 'triggered') {
|
||||
return 'hass:alert-circle';
|
||||
}
|
||||
// state == 'disarmed'
|
||||
@@ -151,8 +151,8 @@ class HaStateLabelBadge extends
|
||||
case 'updater':
|
||||
return stateIcon(state);
|
||||
case 'sun':
|
||||
return state.state === 'above_horizon' ?
|
||||
domainIcon(domain) : 'hass:brightness-3';
|
||||
return state.state === 'above_horizon'
|
||||
? domainIcon(domain) : 'hass:brightness-3';
|
||||
case 'timer':
|
||||
return state.state === 'active' ? 'hass:timer' : 'hass:timer-off';
|
||||
default:
|
||||
@@ -166,8 +166,8 @@ class HaStateLabelBadge extends
|
||||
|
||||
computeLabel(localize, state, _timerTimeRemaining) {
|
||||
const domain = computeStateDomain(state);
|
||||
if (state.state === 'unavailable' ||
|
||||
['device_tracker', 'alarm_control_panel'].includes(domain)) {
|
||||
if (state.state === 'unavailable'
|
||||
|| ['device_tracker', 'alarm_control_panel'].includes(domain)) {
|
||||
// Localize the state with a special state_badge namespace, which has variations of
|
||||
// the state translations that are truncated to fit within the badge label. Translations
|
||||
// are only added for device_tracker and alarm_control_panel.
|
||||
|
@@ -44,17 +44,15 @@ const PRIORITY = {
|
||||
mailbox: 7,
|
||||
};
|
||||
|
||||
const getPriority = domain =>
|
||||
((domain in PRIORITY) ? PRIORITY[domain] : 100);
|
||||
const getPriority = domain => ((domain in PRIORITY) ? PRIORITY[domain] : 100);
|
||||
|
||||
const sortPriority = (domainA, domainB) =>
|
||||
domainA.priority - domainB.priority;
|
||||
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();
|
||||
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;
|
||||
@@ -187,7 +185,7 @@ class HaCards extends PolymerElement {
|
||||
this.$.main.parentNode.removeChild(this.$.main);
|
||||
}
|
||||
return;
|
||||
} else if (!this.$.main.parentNode && this.$.main._parentNode) {
|
||||
} if (!this.$.main.parentNode && this.$.main._parentNode) {
|
||||
this.$.main._parentNode.appendChild(this.$.main);
|
||||
}
|
||||
this._debouncer = Debouncer.debounce(
|
||||
@@ -284,8 +282,8 @@ class HaCards extends PolymerElement {
|
||||
|
||||
const splitted = splitByGroups(states);
|
||||
if (orderedGroupEntities) {
|
||||
splitted.groups.sort((gr1, gr2) => orderedGroupEntities[gr1.entity_id] -
|
||||
orderedGroupEntities[gr2.entity_id]);
|
||||
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);
|
||||
}
|
||||
@@ -332,8 +330,8 @@ class HaCards extends PolymerElement {
|
||||
cards.badges.push.apply(cards.badges, domain.states);
|
||||
});
|
||||
|
||||
cards.badges.sort((e1, e2) => orderedGroupEntities[e1.entity_id] -
|
||||
orderedGroupEntities[e2.entity_id]);
|
||||
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);
|
||||
|
@@ -72,9 +72,11 @@ class HaClimateControl extends EventsMixin(PolymerElement) {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
temperatureStateInFlux(inFlux) {
|
||||
this.$.target_temperature.classList.toggle('in-flux', inFlux);
|
||||
}
|
||||
|
||||
incrementValue() {
|
||||
const newval = this.value + this.step;
|
||||
if (this.value < this.max) {
|
||||
@@ -94,6 +96,7 @@ class HaClimateControl extends EventsMixin(PolymerElement) {
|
||||
this.value = this.max;
|
||||
}
|
||||
}
|
||||
|
||||
decrementValue() {
|
||||
const newval = this.value - this.step;
|
||||
if (this.value > this.min) {
|
||||
@@ -106,6 +109,7 @@ class HaClimateControl extends EventsMixin(PolymerElement) {
|
||||
this.value = this.min;
|
||||
}
|
||||
}
|
||||
|
||||
valueChanged() {
|
||||
// when the last_changed timestamp is changed,
|
||||
// trigger a potential event fire in
|
||||
|
@@ -71,15 +71,15 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
|
||||
computeTarget(hass, stateObj) {
|
||||
if (!hass || !stateObj) return null;
|
||||
// We're using "!= null" on purpose so that we match both null and undefined.
|
||||
if (stateObj.attributes.target_temp_low != null &&
|
||||
stateObj.attributes.target_temp_high != null) {
|
||||
if (stateObj.attributes.target_temp_low != null
|
||||
&& stateObj.attributes.target_temp_high != null) {
|
||||
return `${stateObj.attributes.target_temp_low} - ${stateObj.attributes.target_temp_high} ${hass.config.unit_system.temperature}`;
|
||||
} else if (stateObj.attributes.temperature != null) {
|
||||
} if (stateObj.attributes.temperature != null) {
|
||||
return `${stateObj.attributes.temperature} ${hass.config.unit_system.temperature}`;
|
||||
} else if (stateObj.attributes.target_humidity_low != null &&
|
||||
stateObj.attributes.target_humidity_high != null) {
|
||||
} if (stateObj.attributes.target_humidity_low != null
|
||||
&& stateObj.attributes.target_humidity_high != null) {
|
||||
return `${stateObj.attributes.target_humidity_low} - ${stateObj.attributes.target_humidity_high} %`;
|
||||
} else if (stateObj.attributes.humidity != null) {
|
||||
} if (stateObj.attributes.humidity != null) {
|
||||
return `${stateObj.attributes.humidity} %`;
|
||||
}
|
||||
|
||||
|
@@ -302,9 +302,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
|
||||
applyHsColor(hs) {
|
||||
// do nothing is we already have the same color
|
||||
if (this.hsColor &&
|
||||
this.hsColor.h === hs.h &&
|
||||
this.hsColor.s === hs.s) {
|
||||
if (this.hsColor
|
||||
&& this.hsColor.h === hs.h
|
||||
&& this.hsColor.s === hs.s) {
|
||||
return;
|
||||
}
|
||||
this.setMarkerOnColor(hs); // marker is always set on 'raw' hs position
|
||||
|
@@ -38,25 +38,31 @@ class HaCoverControls extends PolymerElement {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
computeEntityObj(hass, stateObj) {
|
||||
return new CoverEntity(hass, stateObj);
|
||||
}
|
||||
|
||||
computeOpenDisabled(stateObj, entityObj) {
|
||||
var assumedState = stateObj.attributes.assumed_state === true;
|
||||
return (entityObj.isFullyOpen || entityObj.isOpening) && !assumedState;
|
||||
}
|
||||
|
||||
computeClosedDisabled(stateObj, entityObj) {
|
||||
var assumedState = (stateObj.attributes.assumed_state === true);
|
||||
return (entityObj.isFullyClosed || entityObj.isClosing) && !assumedState;
|
||||
}
|
||||
|
||||
onOpenTap(ev) {
|
||||
ev.stopPropagation();
|
||||
this.entityObj.openCover();
|
||||
}
|
||||
|
||||
onCloseTap(ev) {
|
||||
ev.stopPropagation();
|
||||
this.entityObj.closeCover();
|
||||
}
|
||||
|
||||
onStopTap(ev) {
|
||||
ev.stopPropagation();
|
||||
this.entityObj.stopCover();
|
||||
|
@@ -37,25 +37,31 @@ class HaCoverTiltControls extends PolymerElement {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
computeEntityObj(hass, stateObj) {
|
||||
return new CoverEntity(hass, stateObj);
|
||||
}
|
||||
|
||||
computeOpenDisabled(stateObj, entityObj) {
|
||||
var assumedState = stateObj.attributes.assumed_state === true;
|
||||
return entityObj.isFullyOpenTilt && !assumedState;
|
||||
}
|
||||
|
||||
computeClosedDisabled(stateObj, entityObj) {
|
||||
var assumedState = (stateObj.attributes.assumed_state === true);
|
||||
return entityObj.isFullyClosedTilt && !assumedState;
|
||||
}
|
||||
|
||||
onOpenTiltTap(ev) {
|
||||
ev.stopPropagation();
|
||||
this.entityObj.openCoverTilt();
|
||||
}
|
||||
|
||||
onCloseTiltTap(ev) {
|
||||
ev.stopPropagation();
|
||||
this.entityObj.closeCoverTilt();
|
||||
}
|
||||
|
||||
onStopTiltTap(ev) {
|
||||
ev.stopPropagation();
|
||||
this.entityObj.stopCoverTilt();
|
||||
|
@@ -20,6 +20,10 @@ class HaForm extends EventsMixin(PolymerElement) {
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
paper-checkbox {
|
||||
display: inline-block;
|
||||
padding: 22px 0;
|
||||
}
|
||||
</style>
|
||||
<template is="dom-if" if="[[_isArray(schema)]]" restamp="">
|
||||
<template is="dom-if" if="[[error.base]]">
|
||||
@@ -96,7 +100,9 @@ class HaForm extends EventsMixin(PolymerElement) {
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if="[[_equals(schema.type, "boolean")]]" restamp="">
|
||||
<paper-checkbox checked="{{data}}">[[computeLabel(schema)]]</paper-checkbox>
|
||||
<div>
|
||||
<paper-checkbox checked="{{data}}">[[computeLabel(schema)]]</paper-checkbox>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if="[[_equals(schema.type, "select")]]" restamp="">
|
||||
|
@@ -10,7 +10,6 @@ class HaLabeledSlider extends PolymerElement {
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
|
@@ -73,8 +73,8 @@ class HaMarkdown extends EventsMixin(PolymerElement) {
|
||||
const node = walker.currentNode;
|
||||
|
||||
// Open external links in a new window
|
||||
if (node.tagName === 'A' &&
|
||||
node.host !== document.location.host) {
|
||||
if (node.tagName === 'A'
|
||||
&& node.host !== document.location.host) {
|
||||
node.target = '_blank';
|
||||
|
||||
// Fire a resize event when images loaded to notify content resized
|
||||
|
@@ -11,8 +11,7 @@ class HaPaperSlider extends PaperSliderClass {
|
||||
static get template() {
|
||||
const tpl = document.createElement('template');
|
||||
tpl.innerHTML = PaperSliderClass.template.innerHTML;
|
||||
const styleEl = tpl.content.querySelector('style');
|
||||
styleEl.setAttribute('include', 'paper-slider');
|
||||
const styleEl = document.createElement('style');
|
||||
styleEl.innerHTML = `
|
||||
.pin > .slider-knob > .slider-knob-inner {
|
||||
font-size: var(--ha-paper-slider-pin-font-size, 10px);
|
||||
@@ -60,6 +59,7 @@ class HaPaperSlider extends PaperSliderClass {
|
||||
transform: scale(1) translate(0, -10px);
|
||||
}
|
||||
`;
|
||||
tpl.content.appendChild(styleEl);
|
||||
return tpl;
|
||||
}
|
||||
}
|
||||
|
@@ -5,10 +5,10 @@ import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
import EventsMixin from '../mixins/events-mixin.js';
|
||||
|
||||
export const pushSupported = (
|
||||
'serviceWorker' in navigator && 'PushManager' in window &&
|
||||
(document.location.protocol === 'https:' ||
|
||||
document.location.hostname === 'localhost' ||
|
||||
document.location.hostname === '127.0.0.1'));
|
||||
'serviceWorker' in navigator && 'PushManager' in window
|
||||
&& (document.location.protocol === 'https:'
|
||||
|| document.location.hostname === 'localhost'
|
||||
|| document.location.hostname === '127.0.0.1'));
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
|
@@ -34,7 +34,7 @@ class HaServicePicker extends LocalizeMixin(PolymerElement) {
|
||||
if (!hass) {
|
||||
this._services = [];
|
||||
return;
|
||||
} else if (oldHass && hass.services === oldHass.services) {
|
||||
} if (oldHass && hass.services === oldHass.services) {
|
||||
return;
|
||||
}
|
||||
const result = [];
|
||||
|
@@ -6,7 +6,7 @@ import '@polymer/paper-item/paper-item.js';
|
||||
import '@polymer/paper-listbox/paper-listbox.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
import '../components/ha-icon.js';
|
||||
import './ha-icon.js';
|
||||
|
||||
import '../util/hass-translation.js';
|
||||
import LocalizeMixin from '../mixins/localize-mixin.js';
|
||||
@@ -47,7 +47,7 @@ class HaSidebar extends LocalizeMixin(PolymerElement) {
|
||||
}
|
||||
|
||||
paper-listbox {
|
||||
padding-bottom: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
paper-listbox > a {
|
||||
@@ -59,15 +59,37 @@ class HaSidebar extends LocalizeMixin(PolymerElement) {
|
||||
};
|
||||
}
|
||||
|
||||
paper-icon-item {
|
||||
margin: 8px;
|
||||
padding-left: 9px;
|
||||
border-radius: 4px;
|
||||
--paper-item-min-height: 40px;
|
||||
}
|
||||
|
||||
.iron-selected paper-icon-item:before {
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
background-color: var(--sidebar-selected-icon-color);
|
||||
opacity: 0.12;
|
||||
transition: opacity 15ms linear;
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
.iron-selected paper-icon-item[pressed]:before {
|
||||
opacity: 0.37;
|
||||
}
|
||||
|
||||
paper-icon-item span {
|
||||
@apply --sidebar-text;
|
||||
}
|
||||
|
||||
a.iron-selected {
|
||||
--paper-icon-item: {
|
||||
background-color: var(--sidebar-selected-background-color, var(--paper-grey-200));
|
||||
};
|
||||
|
||||
--paper-item-icon: {
|
||||
color: var(--sidebar-selected-icon-color);
|
||||
};
|
||||
@@ -129,7 +151,7 @@ class HaSidebar extends LocalizeMixin(PolymerElement) {
|
||||
</app-toolbar>
|
||||
|
||||
<paper-listbox attr-for-selected="data-panel" selected="[[hass.panelUrl]]">
|
||||
<a href='[[_computeUrl(defaultPage)]]' data-panel$="[[defaultPage]]">
|
||||
<a href='[[_computeUrl(defaultPage)]]' data-panel$="[[defaultPage]]" tabindex="-1">
|
||||
<paper-icon-item>
|
||||
<ha-icon slot="item-icon" icon="hass:apps"></ha-icon>
|
||||
<span class="item-text">[[localize('panel.states')]]</span>
|
||||
@@ -137,7 +159,7 @@ class HaSidebar extends LocalizeMixin(PolymerElement) {
|
||||
</a>
|
||||
|
||||
<template is="dom-repeat" items="[[panels]]">
|
||||
<a href='[[_computeUrl(item.url_path)]]' data-panel$='[[item.url_path]]'>
|
||||
<a href='[[_computeUrl(item.url_path)]]' data-panel$='[[item.url_path]]' tabindex="-1">
|
||||
<paper-icon-item>
|
||||
<ha-icon slot="item-icon" icon="[[item.icon]]"></ha-icon>
|
||||
<span class="item-text">[[_computePanelName(localize, item)]]</span>
|
||||
@@ -159,14 +181,14 @@ class HaSidebar extends LocalizeMixin(PolymerElement) {
|
||||
<div class="subheader">[[localize('ui.sidebar.developer_tools')]]</div>
|
||||
|
||||
<div class="dev-tools layout horizontal justified">
|
||||
<a href="/dev-service">
|
||||
<a href="/dev-service" tabindex="-1">
|
||||
<paper-icon-button
|
||||
icon="hass:remote"
|
||||
alt="[[localize('panel.dev-services')]]"
|
||||
title="[[localize('panel.dev-services')]]"
|
||||
></paper-icon-button>
|
||||
</a>
|
||||
<a href="/dev-state">
|
||||
<a href="/dev-state" tabindex="-1">
|
||||
<paper-icon-button
|
||||
icon="hass:code-tags"
|
||||
alt="[[localize('panel.dev-states')]]"
|
||||
@@ -174,7 +196,7 @@ class HaSidebar extends LocalizeMixin(PolymerElement) {
|
||||
|
||||
></paper-icon-button>
|
||||
</a>
|
||||
<a href="/dev-event">
|
||||
<a href="/dev-event" tabindex="-1">
|
||||
<paper-icon-button
|
||||
icon="hass:radio-tower"
|
||||
alt="[[localize('panel.dev-events')]]"
|
||||
@@ -182,7 +204,7 @@ class HaSidebar extends LocalizeMixin(PolymerElement) {
|
||||
|
||||
></paper-icon-button>
|
||||
</a>
|
||||
<a href="/dev-template">
|
||||
<a href="/dev-template" tabindex="-1">
|
||||
<paper-icon-button
|
||||
icon="hass:file-xml"
|
||||
alt="[[localize('panel.dev-templates')]]"
|
||||
@@ -191,7 +213,7 @@ class HaSidebar extends LocalizeMixin(PolymerElement) {
|
||||
></paper-icon-button>
|
||||
</a>
|
||||
<template is="dom-if" if="[[_mqttLoaded(hass)]]">
|
||||
<a href="/dev-mqtt">
|
||||
<a href="/dev-mqtt" tabindex="-1">
|
||||
<paper-icon-button
|
||||
icon="hass:altimeter"
|
||||
alt="[[localize('panel.dev-mqtt')]]"
|
||||
@@ -200,7 +222,7 @@ class HaSidebar extends LocalizeMixin(PolymerElement) {
|
||||
></paper-icon-button>
|
||||
</a>
|
||||
</template>
|
||||
<a href="/dev-info">
|
||||
<a href="/dev-info" tabindex="-1">
|
||||
<paper-icon-button
|
||||
icon="hass:information-outline"
|
||||
alt="[[localize('panel.dev-info')]]"
|
||||
@@ -283,9 +305,9 @@ class HaSidebar extends LocalizeMixin(PolymerElement) {
|
||||
|
||||
if (aBuiltIn && bBuiltIn) {
|
||||
return sortValue[a.component_name] - sortValue[b.component_name];
|
||||
} else if (aBuiltIn) {
|
||||
} if (aBuiltIn) {
|
||||
return -1;
|
||||
} else if (bBuiltIn) {
|
||||
} if (bBuiltIn) {
|
||||
return 1;
|
||||
}
|
||||
// both not built in, sort by title
|
||||
|
31
src/components/ha-slider.js
Normal file
31
src/components/ha-slider.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import '@polymer/paper-slider';
|
||||
|
||||
const PaperSliderClass = customElements.get('paper-slider');
|
||||
|
||||
class HaSlider extends PaperSliderClass {
|
||||
_calcStep(value) {
|
||||
if (!this.step) {
|
||||
return parseFloat(value);
|
||||
}
|
||||
|
||||
const numSteps = Math.round((value - this.min) / this.step);
|
||||
const stepStr = this.step.toString();
|
||||
const stepPointAt = stepStr.indexOf('.');
|
||||
if (stepPointAt !== -1) {
|
||||
/**
|
||||
* For small values of this.step, if we calculate the step using
|
||||
* For non-integer values of this.step, if we calculate the step using
|
||||
* `Math.round(value / step) * step` we may hit a precision point issue
|
||||
* eg. 0.1 * 0.2 = 0.020000000000000004
|
||||
* http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
|
||||
*
|
||||
* as a work around we can round with the decimal precision of `step`
|
||||
*/
|
||||
const precision = 10 ** (stepStr.length - stepPointAt - 1);
|
||||
return Math.round((numSteps * this.step + this.min) * precision) / precision;
|
||||
}
|
||||
|
||||
return numSteps * this.step + this.min;
|
||||
}
|
||||
}
|
||||
customElements.define('ha-slider', HaSlider);
|
@@ -32,8 +32,8 @@ class HaStartVoiceButton extends EventsMixin(PolymerElement) {
|
||||
}
|
||||
|
||||
computeCanListen(hass) {
|
||||
return ('webkitSpeechRecognition' in window &&
|
||||
isComponentLoaded(hass, 'conversation'));
|
||||
return ('webkitSpeechRecognition' in window
|
||||
&& isComponentLoaded(hass, 'conversation'));
|
||||
}
|
||||
|
||||
handleListenClick() {
|
||||
|
@@ -40,4 +40,3 @@ class HaTextarea extends PolymerElement {
|
||||
}
|
||||
|
||||
customElements.define('ha-textarea', HaTextarea);
|
||||
|
||||
|
@@ -74,8 +74,8 @@ class HaVacuumState extends LocalizeMixin(PolymerElement) {
|
||||
}
|
||||
|
||||
_computeLabel(state, interceptable) {
|
||||
return interceptable ?
|
||||
this.localize(`ui.card.vacuum.actions.${STATES_INTERCEPTABLE[state].action}`)
|
||||
return interceptable
|
||||
? this.localize(`ui.card.vacuum.actions.${STATES_INTERCEPTABLE[state].action}`)
|
||||
: this.localize(`state.vacuum.${state}`);
|
||||
}
|
||||
|
||||
|
@@ -42,6 +42,7 @@ class StateHistoryChartLine extends PolymerElement {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static get observers() {
|
||||
return ['dataChanged(data, endTime, isSingleDevice)'];
|
||||
}
|
||||
@@ -85,9 +86,11 @@ class StateHistoryChartLine extends PolymerElement {
|
||||
return isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
endTime = this.endTime ||
|
||||
new Date(Math.max.apply(null, deviceStates.map(states =>
|
||||
new Date(states.states[states.states.length - 1].last_changed))));
|
||||
|
||||
endTime = this.endTime
|
||||
// Get the highest date from the last date of each device
|
||||
|| new Date(Math.max.apply(null, deviceStates.map(devSts =>
|
||||
new Date(devSts.states[devSts.states.length - 1].last_changed))));
|
||||
if (endTime > new Date()) {
|
||||
endTime = new Date();
|
||||
}
|
||||
@@ -138,8 +141,8 @@ class StateHistoryChartLine extends PolymerElement {
|
||||
// range versus ones that have just a target temperature
|
||||
|
||||
// Using step chart by step-before so manually interpolation not needed.
|
||||
const hasTargetRange = states.states.some(state => state.attributes &&
|
||||
state.attributes.target_temp_high !== state.attributes.target_temp_low);
|
||||
const hasTargetRange = states.states.some(state => state.attributes
|
||||
&& state.attributes.target_temp_high !== state.attributes.target_temp_low);
|
||||
const hasHeat = states.states.some(state => state.state === 'heat');
|
||||
const hasCool = states.states.some(state => state.state === 'cool');
|
||||
|
||||
@@ -208,8 +211,8 @@ class StateHistoryChartLine extends PolymerElement {
|
||||
const dateTime = date.getTime();
|
||||
const lastNullDateTime = lastNullDate.getTime();
|
||||
const lastDateTime = lastDate.getTime();
|
||||
const tmpValue = ((value - lastValue) *
|
||||
((lastNullDateTime - lastDateTime) / (dateTime - lastDateTime))) + lastValue;
|
||||
const tmpValue = ((value - lastValue)
|
||||
* ((lastNullDateTime - lastDateTime) / (dateTime - lastDateTime))) + lastValue;
|
||||
pushData(lastNullDate, [tmpValue]);
|
||||
pushData(new Date(lastNullDateTime + 1), [null]);
|
||||
pushData(date, [value]);
|
||||
|
@@ -83,8 +83,8 @@ class StateHistoryChartTimeline extends PolymerElement {
|
||||
));
|
||||
|
||||
// end time is Math.max(startTime, last_event)
|
||||
let endTime = this.endTime ||
|
||||
new Date(stateHistory.reduce((maxTime, stateInfo) => Math.max(
|
||||
let endTime = this.endTime
|
||||
|| new Date(stateHistory.reduce((maxTime, stateInfo) => Math.max(
|
||||
maxTime,
|
||||
new Date(stateInfo.data[stateInfo.data.length - 1].last_changed)
|
||||
), startTime));
|
||||
|
@@ -67,9 +67,9 @@ class StateHistoryCharts extends LocalizeMixin(PolymerElement) {
|
||||
}
|
||||
|
||||
_computeIsEmpty(isLoadingData, historyData) {
|
||||
const historyDataEmpty = (!historyData || !historyData.timeline || !historyData.line ||
|
||||
(historyData.timeline.length === 0 &&
|
||||
historyData.line.length === 0));
|
||||
const historyDataEmpty = (!historyData || !historyData.timeline || !historyData.line
|
||||
|| (historyData.timeline.length === 0
|
||||
&& historyData.line.length === 0));
|
||||
return !isLoadingData && historyDataEmpty;
|
||||
}
|
||||
|
||||
|
@@ -236,8 +236,8 @@ class HaStateHistoryData extends LocalizeMixin(PolymerElement) {
|
||||
const oldLine = cacheLines.find(cacheLine => cacheLine.unit === unit);
|
||||
if (oldLine) {
|
||||
line.data.forEach((entity) => {
|
||||
const oldEntity =
|
||||
oldLine.data.find(cacheEntity => entity.entity_id === cacheEntity.entity_id);
|
||||
const oldEntity = oldLine.data.find(cacheEntity =>
|
||||
entity.entity_id === cacheEntity.entity_id);
|
||||
if (oldEntity) {
|
||||
oldEntity.states = oldEntity.states.concat(entity.states);
|
||||
} else {
|
||||
@@ -252,8 +252,8 @@ class HaStateHistoryData extends LocalizeMixin(PolymerElement) {
|
||||
|
||||
mergeTimeline(historyTimelines, cacheTimelines) {
|
||||
historyTimelines.forEach((timeline) => {
|
||||
const oldTimeline =
|
||||
cacheTimelines.find(cacheTimeline => cacheTimeline.entity_id === timeline.entity_id);
|
||||
const oldTimeline = cacheTimelines.find(cacheTimeline =>
|
||||
cacheTimeline.entity_id === timeline.entity_id);
|
||||
if (oldTimeline) {
|
||||
oldTimeline.data = oldTimeline.data.concat(timeline.data);
|
||||
} else {
|
||||
|
18
src/data/ws-notifications.js
Normal file
18
src/data/ws-notifications.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createCollection } from 'home-assistant-js-websocket';
|
||||
|
||||
const fetchNotifications = conn => conn.sendMessagePromise({
|
||||
type: 'persistent_notification/get'
|
||||
});
|
||||
|
||||
const subscribeUpdates = (conn, store) => conn.subscribeEvents(
|
||||
() => fetchNotifications(conn).then(ntf => store.setState(ntf, true)),
|
||||
'persistent_notifications_updated'
|
||||
);
|
||||
|
||||
export const subscribeNotifications = (conn, onChange) => createCollection(
|
||||
'_ntf',
|
||||
fetchNotifications,
|
||||
subscribeUpdates,
|
||||
conn,
|
||||
onChange
|
||||
);
|
@@ -1,10 +1,9 @@
|
||||
import { createCollection } from 'home-assistant-js-websocket';
|
||||
|
||||
export const subscribePanels = (conn, onChange) =>
|
||||
createCollection(
|
||||
'_pnl',
|
||||
conn_ => conn_.sendMessagePromise({ type: 'get_panels' }),
|
||||
null,
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
export const subscribePanels = (conn, onChange) => createCollection(
|
||||
'_pnl',
|
||||
conn_ => conn_.sendMessagePromise({ type: 'get_panels' }),
|
||||
null,
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
@@ -4,17 +4,15 @@ const fetchThemes = conn => conn.sendMessagePromise({
|
||||
type: 'frontend/get_themes'
|
||||
});
|
||||
|
||||
const subscribeUpdates = (conn, store) =>
|
||||
conn.subscribeEvents(
|
||||
event => store.setState(event.data, true),
|
||||
'themes_updated'
|
||||
);
|
||||
const subscribeUpdates = (conn, store) => conn.subscribeEvents(
|
||||
event => store.setState(event.data, true),
|
||||
'themes_updated'
|
||||
);
|
||||
|
||||
export const subscribeThemes = (conn, onChange) =>
|
||||
createCollection(
|
||||
'_thm',
|
||||
fetchThemes,
|
||||
subscribeUpdates,
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
export const subscribeThemes = (conn, onChange) => createCollection(
|
||||
'_thm',
|
||||
fetchThemes,
|
||||
subscribeUpdates,
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
@@ -1,10 +1,9 @@
|
||||
import { createCollection, getUser } from 'home-assistant-js-websocket';
|
||||
|
||||
export const subscribeUser = (conn, onChange) =>
|
||||
createCollection(
|
||||
'_usr',
|
||||
conn_ => getUser(conn_),
|
||||
null,
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
export const subscribeUser = (conn, onChange) => createCollection(
|
||||
'_usr',
|
||||
conn_ => getUser(conn_),
|
||||
null,
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
@@ -145,8 +145,8 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) {
|
||||
this.opened = true;
|
||||
}));
|
||||
|
||||
if (!isComponentLoaded(this.hass, 'config.entity_registry') ||
|
||||
(oldVal && oldVal.entity_id === newVal.entity_id)) {
|
||||
if (!isComponentLoaded(this.hass, 'config.entity_registry')
|
||||
|| (oldVal && oldVal.entity_id === newVal.entity_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -134,8 +134,8 @@ class MoreInfoAlarmControlPanel extends LocalizeMixin(EventsMixin(PolymerElement
|
||||
const props = {
|
||||
_codeFormat: newVal.attributes.code_format,
|
||||
_armVisible: state === 'disarmed',
|
||||
_disarmVisible: this._armedStates.includes(state) ||
|
||||
state === 'pending' || state === 'triggered' || state === 'arming'
|
||||
_disarmVisible: this._armedStates.includes(state)
|
||||
|| state === 'pending' || state === 'triggered' || state === 'arming'
|
||||
};
|
||||
props._inputEnabled = props._disarmVisible || props._armVisible;
|
||||
this.setProperties(props);
|
||||
|
@@ -63,9 +63,9 @@ class MoreInfoCamera extends EventsMixin(PolymerElement) {
|
||||
computeCameraImageUrl(hass, stateObj, isVisible) {
|
||||
if (hass.demo) {
|
||||
return '/demo/webcam.jpg';
|
||||
} else if (stateObj && isVisible) {
|
||||
return '/api/camera_proxy_stream/' + stateObj.entity_id +
|
||||
'?token=' + stateObj.attributes.access_token;
|
||||
} if (stateObj && isVisible) {
|
||||
return '/api/camera_proxy_stream/' + stateObj.entity_id
|
||||
+ '?token=' + stateObj.attributes.access_token;
|
||||
}
|
||||
// Return an empty image if no stateObj (= dialog not open) or in cleanup mode.
|
||||
return emptyImageBase64;
|
||||
|
@@ -267,8 +267,9 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
// force polymer to recognize selected item change (to update actual label)
|
||||
this.operationIndex = -1;
|
||||
if (this.stateObj.attributes.operation_list) {
|
||||
this.operationIndex =
|
||||
this.stateObj.attributes.operation_list.indexOf(this.stateObj.attributes.operation_mode);
|
||||
this.operationIndex = (
|
||||
this.stateObj.attributes.operation_list.indexOf(this.stateObj.attributes.operation_mode)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,8 +277,9 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
// force polymer to recognize selected item change (to update actual label)
|
||||
this.swingIndex = -1;
|
||||
if (this.stateObj.attributes.swing_list) {
|
||||
this.swingIndex =
|
||||
this.stateObj.attributes.swing_list.indexOf(this.stateObj.attributes.swing_mode);
|
||||
this.swingIndex = (
|
||||
this.stateObj.attributes.swing_list.indexOf(this.stateObj.attributes.swing_mode)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,23 +287,24 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
// force polymer to recognize selected item change (to update actual label)
|
||||
this.fanIndex = -1;
|
||||
if (this.stateObj.attributes.fan_list) {
|
||||
this.fanIndex =
|
||||
this.stateObj.attributes.fan_list.indexOf(this.stateObj.attributes.fan_mode);
|
||||
this.fanIndex = (
|
||||
this.stateObj.attributes.fan_list.indexOf(this.stateObj.attributes.fan_mode)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
computeTemperatureStepSize(hass, stateObj) {
|
||||
if (stateObj.attributes.target_temp_step) {
|
||||
return stateObj.attributes.target_temp_step;
|
||||
} else if (hass.config.unit_system.temperature.indexOf('F') !== -1) {
|
||||
} if (hass.config.unit_system.temperature.indexOf('F') !== -1) {
|
||||
return 1;
|
||||
}
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
supportsTemperatureControls(stateObj) {
|
||||
return this.supportsTemperature(stateObj) ||
|
||||
this.supportsTemperatureRange(stateObj);
|
||||
return this.supportsTemperature(stateObj)
|
||||
|| this.supportsTemperatureRange(stateObj);
|
||||
}
|
||||
|
||||
supportsTemperature(stateObj) {
|
||||
|
@@ -103,8 +103,8 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
if (newVal) {
|
||||
this.setProperties({
|
||||
oscillationToggleChecked: newVal.attributes.oscillating,
|
||||
speedIndex: newVal.attributes.speed_list ?
|
||||
newVal.attributes.speed_list.indexOf(newVal.attributes.speed) : -1,
|
||||
speedIndex: newVal.attributes.speed_list
|
||||
? newVal.attributes.speed_list.indexOf(newVal.attributes.speed) : -1,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -6,7 +6,6 @@ import '../../../state-summary/state-card-content.js';
|
||||
|
||||
import computeStateDomain from '../../../common/entity/compute_state_domain';
|
||||
import dynamicContentUpdater from '../../../common/dom/dynamic_content_updater.js';
|
||||
import stateMoreInfoType from '../../../common/entity/state_more_info_type.js';
|
||||
|
||||
class MoreInfoGroup extends PolymerElement {
|
||||
static get template() {
|
||||
@@ -70,10 +69,11 @@ class MoreInfoGroup extends PolymerElement {
|
||||
|
||||
statesChanged(stateObj, states) {
|
||||
let groupDomainStateObj = false;
|
||||
let groupDomain = false;
|
||||
|
||||
if (states && states.length > 0) {
|
||||
const baseStateObj = states.find(s => s.state === 'on') || states[0];
|
||||
const groupDomain = computeStateDomain(baseStateObj);
|
||||
groupDomain = computeStateDomain(baseStateObj);
|
||||
|
||||
// Groups need to be filtered out or we'll show content of
|
||||
// first child above the children of the current group
|
||||
@@ -100,7 +100,7 @@ class MoreInfoGroup extends PolymerElement {
|
||||
} else {
|
||||
dynamicContentUpdater(
|
||||
this.$.groupedControlDetails,
|
||||
'MORE-INFO-' + stateMoreInfoType(groupDomainStateObj).toUpperCase(),
|
||||
'MORE-INFO-' + groupDomain.toUpperCase(),
|
||||
{ stateObj: groupDomainStateObj, hass: this.hass }
|
||||
);
|
||||
}
|
||||
|
@@ -29,9 +29,6 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
return html`
|
||||
<style include="iron-flex"></style>
|
||||
<style>
|
||||
.effect_list {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.effect_list, .brightness, .color_temp, .white_value {
|
||||
max-height: 0px;
|
||||
@@ -61,6 +58,18 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
max-height: 84px;
|
||||
}
|
||||
|
||||
.has-brightness
|
||||
.has-color_temp.is-on,
|
||||
.has-white_value.is-on {
|
||||
margin-top: -16px;
|
||||
}
|
||||
|
||||
.has-brightness .brightness,
|
||||
.has-color_temp.is-on .color_temp,
|
||||
.has-white_value.is-on .white_value {
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.has-color.is-on ha-color-picker {
|
||||
max-height: 500px;
|
||||
overflow: visible;
|
||||
@@ -78,6 +87,7 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div class$="[[computeClassNames(stateObj)]]">
|
||||
|
@@ -194,7 +194,7 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
computePlaybackControlIcon(playerObj) {
|
||||
if (playerObj.isPlaying) {
|
||||
return playerObj.supportsPause ? 'hass:pause' : 'hass:stop';
|
||||
} else if (playerObj.hasMediaControl || playerObj.isOff || playerObj.isIdle) {
|
||||
} if (playerObj.hasMediaControl || playerObj.isOff || playerObj.isIdle) {
|
||||
if (playerObj.hasMediaControl && playerObj.supportsPause && !playerObj.isPaused) {
|
||||
return 'hass:play-pause';
|
||||
}
|
||||
|
@@ -25,8 +25,8 @@ class MoreInfoUpdater extends PolymerElement {
|
||||
}
|
||||
|
||||
computeReleaseNotes(stateObj) {
|
||||
return (stateObj.attributes.release_notes ||
|
||||
'https://www.home-assistant.io/docs/installation/updating/');
|
||||
return (stateObj.attributes.release_notes
|
||||
|| 'https://www.home-assistant.io/docs/installation/updating/');
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -121,7 +121,7 @@ class MoreInfoWeather extends LocalizeMixin(PolymerElement) {
|
||||
cloudy: 'hass:weather-cloudy',
|
||||
fog: 'hass:weather-fog',
|
||||
hail: 'hass:weather-hail',
|
||||
lightning: 'mid:weather-lightning',
|
||||
lightning: 'hass:weather-lightning',
|
||||
'lightning-rainy': 'hass:weather-lightning-rainy',
|
||||
partlycloudy: 'hass:weather-partlycloudy',
|
||||
pouring: 'hass:weather-pouring',
|
||||
|
@@ -55,8 +55,12 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
|
||||
}
|
||||
}
|
||||
|
||||
paper-dialog-scrollable {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
:host([domain=camera]) paper-dialog-scrollable {
|
||||
margin: 0 -24px -5px;
|
||||
margin: 0 -24px -21px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -74,7 +78,7 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
|
||||
<paper-dialog-scrollable dialog-element="[[dialogElement]]">
|
||||
<template is="dom-if" if="[[_computeShowHistoryComponent(hass, stateObj)]]" restamp="">
|
||||
<ha-state-history-data hass="[[hass]]" filter-type="recent-entity" entity-id="[[stateObj.entity_id]]" data="{{_stateHistory}}" is-loading="{{_stateHistoryLoading}}" cache-config="[[_cacheConfig]]"></ha-state-history-data>
|
||||
<state-history-charts hass="[[hass]]" history-data="[[_stateHistory]]" is-loading-data="[[_stateHistoryLoading]]" up-to-now no-single="[[large]]"></state-history-charts>
|
||||
<state-history-charts hass="[[hass]]" history-data="[[_stateHistory]]" is-loading-data="[[_stateHistoryLoading]]" up-to-now></state-history-charts>
|
||||
</template>
|
||||
<more-info-content state-obj="[[stateObj]]" hass="[[hass]]"></more-info-content>
|
||||
</paper-dialog-scrollable>
|
||||
@@ -128,9 +132,9 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
|
||||
}
|
||||
|
||||
_computeShowHistoryComponent(hass, stateObj) {
|
||||
return hass && stateObj &&
|
||||
isComponentLoaded(hass, 'history') &&
|
||||
!DOMAINS_MORE_INFO_NO_HISTORY.includes(computeStateDomain(stateObj));
|
||||
return hass && stateObj
|
||||
&& isComponentLoaded(hass, 'history')
|
||||
&& !DOMAINS_MORE_INFO_NO_HISTORY.includes(computeStateDomain(stateObj));
|
||||
}
|
||||
|
||||
_computeDomain(stateObj) {
|
||||
|
@@ -9,6 +9,7 @@ import EventsMixin from '../../mixins/events-mixin.js';
|
||||
import LocalizeMixin from '../../mixins/localize-mixin.js';
|
||||
|
||||
import computeStateName from '../../common/entity/compute_state_name.js';
|
||||
import computeDomain from '../../common/entity/compute_domain.js';
|
||||
import isComponentLoaded from '../../common/config/is_component_loaded.js';
|
||||
|
||||
/*
|
||||
@@ -44,7 +45,10 @@ class MoreInfoSettings extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
<app-toolbar>
|
||||
<paper-icon-button icon="hass:arrow-left" on-click="_backTapped"></paper-icon-button>
|
||||
<div main-title="">[[_computeStateName(stateObj)]]</div>
|
||||
<paper-button on-click="_save">[[localize('ui.dialogs.more_info_settings.save')]]</paper-button>
|
||||
<paper-button
|
||||
on-click="_save"
|
||||
disabled='[[_computeInvalid(_entityId)]]'
|
||||
>[[localize('ui.dialogs.more_info_settings.save')]]</paper-button>
|
||||
</app-toolbar>
|
||||
|
||||
<div class="form">
|
||||
@@ -55,6 +59,8 @@ class MoreInfoSettings extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
<paper-input
|
||||
value="{{_entityId}}"
|
||||
label="[[localize('ui.dialogs.more_info_settings.entity_id')]]"
|
||||
error-message="Domain needs to stay the same"
|
||||
invalid='[[_computeInvalid(_entityId)]]'
|
||||
></paper-input>
|
||||
</div>
|
||||
`;
|
||||
@@ -90,6 +96,10 @@ class MoreInfoSettings extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
return isComponentLoaded(hass, 'config.entity_registry');
|
||||
}
|
||||
|
||||
_computeInvalid(entityId) {
|
||||
return computeDomain(this.stateObj.entity_id) !== computeDomain(entityId);
|
||||
}
|
||||
|
||||
_registryInfoChanged(newVal) {
|
||||
if (newVal) {
|
||||
this.setProperties({
|
||||
|
@@ -18,8 +18,7 @@ import '../layouts/app/home-assistant.js';
|
||||
|
||||
/* polyfill for paper-dropdown */
|
||||
setTimeout(
|
||||
() =>
|
||||
import(/* webpackChunkName: "polyfill-web-animations-next" */ 'web-animations-js/web-animations-next-lite.min.js'),
|
||||
() => import(/* webpackChunkName: "polyfill-web-animations-next" */ 'web-animations-js/web-animations-next-lite.min.js'),
|
||||
2000
|
||||
);
|
||||
|
||||
|
@@ -8,5 +8,4 @@ import '../resources/roboto.js';
|
||||
import '../auth/ha-authorize.js';
|
||||
|
||||
/* polyfill for paper-dropdown */
|
||||
setTimeout(() =>
|
||||
import(/* webpackChunkName: "polyfill-web-animations-next" */ 'web-animations-js/web-animations-next-lite.min.js'), 2000);
|
||||
setTimeout(() => import(/* webpackChunkName: "polyfill-web-animations-next" */ 'web-animations-js/web-animations-next-lite.min.js'), 2000);
|
||||
|
@@ -4,6 +4,7 @@ import {
|
||||
subscribeConfig,
|
||||
subscribeEntities,
|
||||
subscribeServices,
|
||||
ERR_INVALID_AUTH,
|
||||
} from 'home-assistant-js-websocket';
|
||||
|
||||
import { loadTokens, saveTokens } from '../common/auth/token_storage.js';
|
||||
@@ -11,21 +12,45 @@ import { subscribePanels } from '../data/ws-panels.js';
|
||||
import { subscribeThemes } from '../data/ws-themes.js';
|
||||
import { subscribeUser } from '../data/ws-user.js';
|
||||
|
||||
window.hassAuth = getAuth({
|
||||
hassUrl: `${location.protocol}//${location.host}`,
|
||||
saveTokens,
|
||||
loadTokens: () => Promise.resolve(loadTokens()),
|
||||
});
|
||||
const hassUrl = `${location.protocol}//${location.host}`;
|
||||
const isExternal = location.search.includes('external_auth=1');
|
||||
|
||||
window.hassConnection = window.hassAuth.then((auth) => {
|
||||
if (location.search.includes('auth_callback=1')) {
|
||||
history.replaceState(null, null, location.pathname);
|
||||
const authProm = isExternal
|
||||
? () => import('../common/auth/external_auth.js')
|
||||
.then(mod => new mod.default(hassUrl))
|
||||
: () => getAuth({
|
||||
hassUrl,
|
||||
saveTokens,
|
||||
loadTokens: () => Promise.resolve(loadTokens()),
|
||||
});
|
||||
|
||||
const connProm = async (auth) => {
|
||||
try {
|
||||
const conn = await createConnection({ auth });
|
||||
|
||||
// Clear url if we have been able to establish a connection
|
||||
if (location.search.includes('auth_callback=1')) {
|
||||
history.replaceState(null, null, location.pathname);
|
||||
}
|
||||
|
||||
return { auth, conn };
|
||||
} catch (err) {
|
||||
if (err !== ERR_INVALID_AUTH) {
|
||||
throw err;
|
||||
}
|
||||
// We can get invalid auth if auth tokens were stored that are no longer valid
|
||||
// Clear stored tokens.
|
||||
if (!isExternal) saveTokens(null);
|
||||
auth = await authProm();
|
||||
const conn = await createConnection({ auth });
|
||||
return { auth, conn };
|
||||
}
|
||||
return createConnection({ auth });
|
||||
});
|
||||
};
|
||||
|
||||
window.hassConnection = authProm().then(connProm);
|
||||
|
||||
// Start fetching some of the data that we will need.
|
||||
window.hassConnection.then((conn) => {
|
||||
window.hassConnection.then(({ conn }) => {
|
||||
const noop = () => {};
|
||||
subscribeEntities(conn, noop);
|
||||
subscribeConfig(conn, noop);
|
||||
|
@@ -4,9 +4,9 @@ import createCustomPanelElement from '../util/custom-panel/create-custom-panel-e
|
||||
import setCustomPanelProperties from '../util/custom-panel/set-custom-panel-properties.js';
|
||||
|
||||
const webComponentsSupported = (
|
||||
'customElements' in window &&
|
||||
'import' in document.createElement('link') &&
|
||||
'content' in document.createElement('template'));
|
||||
'customElements' in window
|
||||
&& 'import' in document.createElement('link')
|
||||
&& 'content' in document.createElement('template'));
|
||||
|
||||
let es5Loaded = null;
|
||||
|
||||
|
@@ -9,8 +9,7 @@ export default superClass => class extends superClass {
|
||||
super.ready();
|
||||
this.addEventListener('hass-logout', () => this._handleLogout());
|
||||
// HACK :( We don't have a way yet to trigger an update of `subscribeUser`
|
||||
this.addEventListener('hass-refresh-current-user', () =>
|
||||
getUser(this.hass.connection).then(user => this._updateHass({ user })));
|
||||
this.addEventListener('hass-refresh-current-user', () => getUser(this.hass.connection).then(user => this._updateHass({ user })));
|
||||
}
|
||||
|
||||
hassConnected() {
|
||||
@@ -27,9 +26,16 @@ export default superClass => class extends superClass {
|
||||
});
|
||||
}
|
||||
|
||||
_handleLogout() {
|
||||
this.hass.connection.close();
|
||||
clearState();
|
||||
document.location.href = '/';
|
||||
async _handleLogout() {
|
||||
try {
|
||||
await this.hass.auth.revoke();
|
||||
this.hass.connection.close();
|
||||
clearState();
|
||||
document.location.href = '/';
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
console.error(err);
|
||||
alert('Log out failed');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -13,141 +13,135 @@ import EventsMixin from '../../mixins/events-mixin.js';
|
||||
|
||||
import { getState } from '../../util/ha-pref-storage.js';
|
||||
import { getActiveTranslation } from '../../util/hass-translation.js';
|
||||
import { fetchWithAuth } from '../../util/fetch-with-auth.js';
|
||||
import hassCallApi from '../../util/hass-call-api.js';
|
||||
import computeStateName from '../../common/entity/compute_state_name.js';
|
||||
import { subscribePanels } from '../../data/ws-panels';
|
||||
|
||||
export default superClass =>
|
||||
class extends EventsMixin(LocalizeMixin(superClass)) {
|
||||
ready() {
|
||||
super.ready();
|
||||
this._handleConnProm();
|
||||
export default superClass => class extends EventsMixin(LocalizeMixin(superClass)) {
|
||||
ready() {
|
||||
super.ready();
|
||||
this._handleConnProm();
|
||||
}
|
||||
|
||||
async _handleConnProm() {
|
||||
let auth;
|
||||
let conn;
|
||||
try {
|
||||
const result = await window.hassConnection;
|
||||
auth = result.auth;
|
||||
conn = result.conn;
|
||||
} catch (err) {
|
||||
this._error = true;
|
||||
return;
|
||||
}
|
||||
|
||||
async _handleConnProm() {
|
||||
const [auth, conn] = await Promise.all([window.hassAuth, window.hassConnection]);
|
||||
this.hass = Object.assign({
|
||||
auth,
|
||||
connection: conn,
|
||||
connected: true,
|
||||
states: null,
|
||||
config: null,
|
||||
themes: null,
|
||||
panels: null,
|
||||
panelUrl: this.panelUrl,
|
||||
|
||||
this.hass = Object.assign({
|
||||
auth,
|
||||
connection: conn,
|
||||
connected: true,
|
||||
states: null,
|
||||
config: null,
|
||||
themes: null,
|
||||
panels: null,
|
||||
panelUrl: this.panelUrl,
|
||||
language: getActiveTranslation(),
|
||||
// If resources are already loaded, don't discard them
|
||||
resources: (this.hass && this.hass.resources) || null,
|
||||
|
||||
language: getActiveTranslation(),
|
||||
// If resources are already loaded, don't discard them
|
||||
resources: (this.hass && this.hass.resources) || null,
|
||||
translationMetadata: translationMetadata,
|
||||
dockedSidebar: false,
|
||||
moreInfoEntityId: null,
|
||||
callService: async (domain, service, serviceData = {}) => {
|
||||
try {
|
||||
await callService(conn, domain, service, serviceData);
|
||||
|
||||
translationMetadata: translationMetadata,
|
||||
dockedSidebar: false,
|
||||
moreInfoEntityId: null,
|
||||
callService: async (domain, service, serviceData = {}) => {
|
||||
try {
|
||||
await callService(conn, domain, service, serviceData);
|
||||
|
||||
let message;
|
||||
let name;
|
||||
if (serviceData.entity_id && this.hass.states &&
|
||||
this.hass.states[serviceData.entity_id]) {
|
||||
name = computeStateName(this.hass.states[serviceData.entity_id]);
|
||||
}
|
||||
if (service === 'turn_on' && serviceData.entity_id) {
|
||||
message = this.localize(
|
||||
'ui.notification_toast.entity_turned_on',
|
||||
'entity', name || serviceData.entity_id
|
||||
);
|
||||
} else if (service === 'turn_off' && serviceData.entity_id) {
|
||||
message = this.localize(
|
||||
'ui.notification_toast.entity_turned_off',
|
||||
'entity', name || serviceData.entity_id
|
||||
);
|
||||
} else {
|
||||
message = this.localize(
|
||||
'ui.notification_toast.service_called',
|
||||
'service', `${domain}/${service}`
|
||||
);
|
||||
}
|
||||
this.fire('hass-notification', { message });
|
||||
} catch (err) {
|
||||
const message = this.localize(
|
||||
'ui.notification_toast.service_call_failed',
|
||||
let message;
|
||||
let name;
|
||||
if (serviceData.entity_id && this.hass.states
|
||||
&& this.hass.states[serviceData.entity_id]) {
|
||||
name = computeStateName(this.hass.states[serviceData.entity_id]);
|
||||
}
|
||||
if (service === 'turn_on' && serviceData.entity_id) {
|
||||
message = this.localize(
|
||||
'ui.notification_toast.entity_turned_on',
|
||||
'entity', name || serviceData.entity_id
|
||||
);
|
||||
} else if (service === 'turn_off' && serviceData.entity_id) {
|
||||
message = this.localize(
|
||||
'ui.notification_toast.entity_turned_off',
|
||||
'entity', name || serviceData.entity_id
|
||||
);
|
||||
} else {
|
||||
message = this.localize(
|
||||
'ui.notification_toast.service_called',
|
||||
'service', `${domain}/${service}`
|
||||
);
|
||||
this.fire('hass-notification', { message });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
callApi: async (method, path, parameters) => {
|
||||
const host = window.location.protocol + '//' + window.location.host;
|
||||
|
||||
try {
|
||||
if (auth.expired) await auth.refreshAccessToken();
|
||||
} catch (err) {
|
||||
if (err === ERR_INVALID_AUTH) {
|
||||
// Trigger auth flow
|
||||
location.reload();
|
||||
// ensure further JS is not executed
|
||||
await new Promise(() => {});
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
return await hassCallApi(host, auth, method, path, parameters);
|
||||
},
|
||||
// For messages that do not get a response
|
||||
sendWS: (msg) => {
|
||||
// eslint-disable-next-line
|
||||
if (__DEV__) console.log('Sending', msg);
|
||||
conn.sendMessage(msg);
|
||||
},
|
||||
// For messages that expect a response
|
||||
callWS: (msg) => {
|
||||
/* eslint-disable no-console */
|
||||
this.fire('hass-notification', { message });
|
||||
} catch (err) {
|
||||
const message = this.localize(
|
||||
'ui.notification_toast.service_call_failed',
|
||||
'service', `${domain}/${service}`
|
||||
);
|
||||
this.fire('hass-notification', { message });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
callApi: async (method, path, parameters) => hassCallApi(auth, method, path, parameters),
|
||||
fetchWithAuth: (path, init) => fetchWithAuth(auth, `${auth.data.hassUrl}${path}`, init),
|
||||
// For messages that do not get a response
|
||||
sendWS: (msg) => {
|
||||
// eslint-disable-next-line
|
||||
if (__DEV__) console.log('Sending', msg);
|
||||
conn.sendMessage(msg);
|
||||
},
|
||||
// For messages that expect a response
|
||||
callWS: (msg) => {
|
||||
/* eslint-disable no-console */
|
||||
if (__DEV__) console.log('Sending', msg);
|
||||
|
||||
const resp = conn.sendMessagePromise(msg);
|
||||
const resp = conn.sendMessagePromise(msg);
|
||||
|
||||
if (__DEV__) {
|
||||
resp.then(
|
||||
result => console.log('Received', result),
|
||||
err => console.log('Error', err),
|
||||
);
|
||||
}
|
||||
return resp;
|
||||
},
|
||||
}, getState());
|
||||
if (__DEV__) {
|
||||
resp.then(
|
||||
result => console.log('Received', result),
|
||||
err => console.log('Error', err),
|
||||
);
|
||||
}
|
||||
return resp;
|
||||
},
|
||||
}, getState());
|
||||
|
||||
this.hassConnected();
|
||||
}
|
||||
this.hassConnected();
|
||||
}
|
||||
|
||||
hassConnected() {
|
||||
super.hassConnected();
|
||||
hassConnected() {
|
||||
super.hassConnected();
|
||||
|
||||
const conn = this.hass.connection;
|
||||
const conn = this.hass.connection;
|
||||
|
||||
conn.addEventListener('ready', () => this.hassReconnected());
|
||||
conn.addEventListener('disconnected', () => this.hassDisconnected());
|
||||
// If we reconnect after losing connection and auth is no longer valid.
|
||||
conn.addEventListener('reconnect-error', (_conn, err) => {
|
||||
if (err === ERR_INVALID_AUTH) location.reload();
|
||||
});
|
||||
conn.addEventListener('ready', () => this.hassReconnected());
|
||||
conn.addEventListener('disconnected', () => this.hassDisconnected());
|
||||
// If we reconnect after losing connection and auth is no longer valid.
|
||||
conn.addEventListener('reconnect-error', (_conn, err) => {
|
||||
if (err === ERR_INVALID_AUTH) location.reload();
|
||||
});
|
||||
|
||||
subscribeEntities(conn, states => this._updateHass({ states }));
|
||||
subscribeConfig(conn, config => this._updateHass({ config }));
|
||||
subscribeServices(conn, services => this._updateHass({ services }));
|
||||
subscribePanels(conn, panels => this._updateHass({ panels }));
|
||||
}
|
||||
subscribeEntities(conn, states => this._updateHass({ states }));
|
||||
subscribeConfig(conn, config => this._updateHass({ config }));
|
||||
subscribeServices(conn, services => this._updateHass({ services }));
|
||||
subscribePanels(conn, panels => this._updateHass({ panels }));
|
||||
}
|
||||
|
||||
hassReconnected() {
|
||||
super.hassReconnected();
|
||||
this._updateHass({ connected: true });
|
||||
}
|
||||
hassReconnected() {
|
||||
super.hassReconnected();
|
||||
this._updateHass({ connected: true });
|
||||
}
|
||||
|
||||
hassDisconnected() {
|
||||
super.hassDisconnected();
|
||||
this._updateHass({ connected: false });
|
||||
}
|
||||
};
|
||||
hassDisconnected() {
|
||||
super.hassDisconnected();
|
||||
this._updateHass({ connected: false });
|
||||
}
|
||||
};
|
||||
|
@@ -1,23 +1,22 @@
|
||||
export default superClass =>
|
||||
class extends superClass {
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener('register-dialog', e => this.registerDialog(e.detail));
|
||||
}
|
||||
export default superClass => class extends superClass {
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener('register-dialog', e => this.registerDialog(e.detail));
|
||||
}
|
||||
|
||||
registerDialog({ dialogShowEvent, dialogTag, dialogImport }) {
|
||||
let loaded = null;
|
||||
registerDialog({ dialogShowEvent, dialogTag, dialogImport }) {
|
||||
let loaded = null;
|
||||
|
||||
this.addEventListener(dialogShowEvent, (showEv) => {
|
||||
if (!loaded) {
|
||||
loaded = dialogImport().then(() => {
|
||||
const dialogEl = document.createElement(dialogTag);
|
||||
this.shadowRoot.appendChild(dialogEl);
|
||||
this.provideHass(dialogEl);
|
||||
return dialogEl;
|
||||
});
|
||||
}
|
||||
loaded.then(dialogEl => dialogEl.showDialog(showEv.detail));
|
||||
});
|
||||
}
|
||||
};
|
||||
this.addEventListener(dialogShowEvent, (showEv) => {
|
||||
if (!loaded) {
|
||||
loaded = dialogImport().then(() => {
|
||||
const dialogEl = document.createElement(dialogTag);
|
||||
this.shadowRoot.appendChild(dialogEl);
|
||||
this.provideHass(dialogEl);
|
||||
return dialogEl;
|
||||
});
|
||||
}
|
||||
loaded.then(dialogEl => dialogEl.showDialog(showEv.detail));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@@ -1,27 +1,26 @@
|
||||
import LocalizeMixin from '../../mixins/localize-mixin.js';
|
||||
|
||||
export default superClass =>
|
||||
class extends LocalizeMixin(superClass) {
|
||||
hassConnected() {
|
||||
super.hassConnected();
|
||||
export default superClass => class extends LocalizeMixin(superClass) {
|
||||
hassConnected() {
|
||||
super.hassConnected();
|
||||
// Need to load in advance because when disconnected, can't dynamically load code.
|
||||
import(/* webpackChunkName: "ha-toast" */ '../../components/ha-toast.js');
|
||||
}
|
||||
}
|
||||
|
||||
hassReconnected() {
|
||||
super.hassReconnected();
|
||||
this.__discToast.opened = false;
|
||||
}
|
||||
hassReconnected() {
|
||||
super.hassReconnected();
|
||||
this.__discToast.opened = false;
|
||||
}
|
||||
|
||||
hassDisconnected() {
|
||||
super.hassDisconnected();
|
||||
if (!this.__discToast) {
|
||||
const el = document.createElement('ha-toast');
|
||||
el.duration = 0;
|
||||
el.text = this.localize('ui.notification_toast.connection_lost');
|
||||
this.__discToast = el;
|
||||
this.shadowRoot.appendChild(el);
|
||||
}
|
||||
this.__discToast.opened = true;
|
||||
hassDisconnected() {
|
||||
super.hassDisconnected();
|
||||
if (!this.__discToast) {
|
||||
const el = document.createElement('ha-toast');
|
||||
el.duration = 0;
|
||||
el.text = this.localize('ui.notification_toast.connection_lost');
|
||||
this.__discToast = el;
|
||||
this.shadowRoot.appendChild(el);
|
||||
}
|
||||
};
|
||||
this.__discToast.opened = true;
|
||||
}
|
||||
};
|
||||
|
@@ -8,9 +8,13 @@ export default superClass => class extends superClass {
|
||||
|
||||
// Exists so all methods can safely call super method
|
||||
hassConnected() {}
|
||||
|
||||
hassReconnected() {}
|
||||
|
||||
hassDisconnected() {}
|
||||
|
||||
panelUrlChanged(newPanelUrl) {}
|
||||
|
||||
hassChanged(hass, oldHass) {
|
||||
this.__provideHass.forEach((el) => {
|
||||
el.hass = hass;
|
||||
|
@@ -5,8 +5,8 @@ import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
|
||||
|
||||
import '../../layouts/home-assistant-main.js';
|
||||
import '../../layouts/ha-init-page.js';
|
||||
import '../home-assistant-main.js';
|
||||
import '../ha-init-page.js';
|
||||
import '../../resources/ha-style.js';
|
||||
import registerServiceWorker from '../../util/register-service-worker.js';
|
||||
|
||||
@@ -51,7 +51,7 @@ class HomeAssistant extends ext(PolymerElement, [
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if="[[!showMain]]" restamp>
|
||||
<ha-init-page></ha-init-page>
|
||||
<ha-init-page error='[[_error]]'></ha-init-page>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
@@ -73,6 +73,10 @@ class HomeAssistant extends ext(PolymerElement, [
|
||||
computed: 'computePanelUrl(routeData)',
|
||||
observer: 'panelUrlChanged',
|
||||
},
|
||||
_error: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -1,22 +1,20 @@
|
||||
import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
|
||||
|
||||
export default superClass =>
|
||||
class extends superClass {
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener('hass-more-info', e => this._handleMoreInfo(e));
|
||||
export default superClass => class extends superClass {
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener('hass-more-info', e => this._handleMoreInfo(e));
|
||||
|
||||
// Load it once we are having the initial rendering done.
|
||||
afterNextRender(null, () =>
|
||||
import(/* webpackChunkName: "more-info-dialog" */ '../../dialogs/ha-more-info-dialog.js'));
|
||||
}
|
||||
// Load it once we are having the initial rendering done.
|
||||
afterNextRender(null, () => import(/* webpackChunkName: "more-info-dialog" */ '../../dialogs/ha-more-info-dialog.js'));
|
||||
}
|
||||
|
||||
async _handleMoreInfo(ev) {
|
||||
if (!this.__moreInfoEl) {
|
||||
this.__moreInfoEl = document.createElement('ha-more-info-dialog');
|
||||
this.shadowRoot.appendChild(this.__moreInfoEl);
|
||||
this.provideHass(this.__moreInfoEl);
|
||||
}
|
||||
this._updateHass({ moreInfoEntityId: ev.detail.entityId });
|
||||
async _handleMoreInfo(ev) {
|
||||
if (!this.__moreInfoEl) {
|
||||
this.__moreInfoEl = document.createElement('ha-more-info-dialog');
|
||||
this.shadowRoot.appendChild(this.__moreInfoEl);
|
||||
this.provideHass(this.__moreInfoEl);
|
||||
}
|
||||
};
|
||||
this._updateHass({ moreInfoEntityId: ev.detail.entityId });
|
||||
}
|
||||
};
|
||||
|
@@ -1,11 +1,10 @@
|
||||
export default superClass =>
|
||||
class extends superClass {
|
||||
ready() {
|
||||
super.ready();
|
||||
this.registerDialog({
|
||||
dialogShowEvent: 'hass-notification',
|
||||
dialogTag: 'notification-manager',
|
||||
dialogImport: () => import(/* webpackChunkName: "notification-manager" */ '../../managers/notification-manager.js'),
|
||||
});
|
||||
}
|
||||
};
|
||||
export default superClass => class extends superClass {
|
||||
ready() {
|
||||
super.ready();
|
||||
this.registerDialog({
|
||||
dialogShowEvent: 'hass-notification',
|
||||
dialogTag: 'notification-manager',
|
||||
dialogImport: () => import(/* webpackChunkName: "notification-manager" */ '../../managers/notification-manager.js'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@@ -1,15 +1,13 @@
|
||||
import { storeState } from '../../util/ha-pref-storage.js';
|
||||
|
||||
export default superClass =>
|
||||
class extends superClass {
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener('hass-dock-sidebar', e =>
|
||||
this._handleDockSidebar(e));
|
||||
}
|
||||
export default superClass => class extends superClass {
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener('hass-dock-sidebar', e => this._handleDockSidebar(e));
|
||||
}
|
||||
|
||||
_handleDockSidebar(ev) {
|
||||
this._updateHass({ dockedSidebar: ev.detail.dock });
|
||||
storeState(this.hass);
|
||||
}
|
||||
};
|
||||
_handleDockSidebar(ev) {
|
||||
this._updateHass({ dockedSidebar: ev.detail.dock });
|
||||
storeState(this.hass);
|
||||
}
|
||||
};
|
||||
|
@@ -1,12 +1,8 @@
|
||||
import '@polymer/iron-flex-layout/iron-flex-layout-classes.js';
|
||||
import '@polymer/paper-button/paper-button.js';
|
||||
import '@polymer/paper-checkbox/paper-checkbox.js';
|
||||
import '@polymer/paper-input/paper-input.js';
|
||||
import '@polymer/paper-spinner/paper-spinner.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
import { ERR_CANNOT_CONNECT, ERR_INVALID_AUTH } from 'home-assistant-js-websocket';
|
||||
|
||||
|
||||
import LocalizeMixin from '../mixins/localize-mixin.js';
|
||||
import EventsMixin from '../mixins/events-mixin.js';
|
||||
@@ -26,82 +22,26 @@ class HaInitPage extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
|
||||
<div class="layout vertical center center-center fit">
|
||||
<img src="/static/icons/favicon-192x192.png" height="192">
|
||||
<paper-spinner active="true"></paper-spinner>
|
||||
Loading data
|
||||
<paper-spinner active="[[!error]]"></paper-spinner>
|
||||
<template is='dom-if' if='[[error]]'>
|
||||
Unable to connect to Home Assistant.
|
||||
<paper-button on-click='_retry'>Retry</paper-button>
|
||||
</template>
|
||||
<template is='dom-if' if='[[!error]]'>
|
||||
Loading data
|
||||
</template>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener('keydown', ev => this.passwordKeyDown(ev));
|
||||
static get properties() {
|
||||
return {
|
||||
error: Boolean,
|
||||
};
|
||||
}
|
||||
|
||||
computeLoadingMsg(isValidating) {
|
||||
return isValidating ? 'Connecting' : 'Loading data';
|
||||
}
|
||||
|
||||
computeShowSpinner(forceShowLoading, isValidating) {
|
||||
return forceShowLoading || isValidating;
|
||||
}
|
||||
|
||||
isValidatingChanged(newVal) {
|
||||
if (!newVal) {
|
||||
setTimeout(() => {
|
||||
if (this.$.passwordInput.inputElement.inputElement) {
|
||||
this.$.passwordInput.inputElement.inputElement.focus();
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
passwordKeyDown(ev) {
|
||||
// validate on enter
|
||||
if (ev.keyCode === 13) {
|
||||
this.validatePassword();
|
||||
ev.preventDefault();
|
||||
// clear error after we start typing again
|
||||
} else if (this.errorMessage) {
|
||||
this.errorMessage = '';
|
||||
}
|
||||
}
|
||||
|
||||
validatePassword() {
|
||||
var auth = this.password;
|
||||
this.$.hideKeyboardOnFocus.focus();
|
||||
const connProm = window.createHassConnection(auth);
|
||||
this.fire('try-connection', { connProm });
|
||||
|
||||
if (this.$.rememberLogin.checked) {
|
||||
connProm.then(function () {
|
||||
localStorage.authToken = auth;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleConnectionPromiseChanged(newVal) {
|
||||
if (!newVal) return;
|
||||
|
||||
var el = this;
|
||||
this.isValidating = true;
|
||||
|
||||
this.connectionPromise.then(
|
||||
function () {
|
||||
el.isValidating = false;
|
||||
el.password = '';
|
||||
},
|
||||
function (errCode) {
|
||||
el.isValidating = false;
|
||||
|
||||
if (errCode === ERR_CANNOT_CONNECT) {
|
||||
el.errorMessage = 'Unable to connect';
|
||||
} else if (errCode === ERR_INVALID_AUTH) {
|
||||
el.errorMessage = 'Invalid password';
|
||||
} else {
|
||||
el.errorMessage = 'Unknown error: ' + errCode;
|
||||
}
|
||||
}
|
||||
);
|
||||
_retry() {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -14,6 +14,7 @@ class HassSubpage extends PolymerElement {
|
||||
<app-toolbar>
|
||||
<paper-icon-button icon="hass:arrow-left" on-click="_backTapped"></paper-icon-button>
|
||||
<div main-title="">[[header]]</div>
|
||||
<slot name="toolbar-icon"></slot>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
|
||||
|
@@ -284,8 +284,8 @@ class PartialCards extends EventsMixin(NavigateMixin(PolymerElement)) {
|
||||
}
|
||||
|
||||
isView(currentView, defaultView) {
|
||||
return (currentView || defaultView) &&
|
||||
this.hass.states[currentView || DEFAULT_VIEW_ENTITY_ID];
|
||||
return (currentView || defaultView)
|
||||
&& this.hass.states[currentView || DEFAULT_VIEW_ENTITY_ID];
|
||||
}
|
||||
|
||||
_defaultViewFilter(hass, entityId) {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user