Merge pull request #2077 from home-assistant/dev

20181121.0
This commit is contained in:
Paulus Schoutsen 2018-11-21 20:14:51 +01:00 committed by GitHub
commit 463c7eae54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
438 changed files with 15614 additions and 10204 deletions

3
.gitignore vendored
View File

@ -22,7 +22,8 @@ bin
dist dist
# vscode # vscode
.vscode .vscode/*
!.vscode/extensions.json
# Secrets # Secrets
.lokalise_token .lokalise_token

7
.vscode/extensions.json vendored Executable file
View File

@ -0,0 +1,7 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"eg2.tslint",
"esbenp.prettier-vscode"
]
}

View File

@ -38,9 +38,9 @@ class DemoCard extends PolymerElement {
} }
</style> </style>
<h2>[[config.heading]]</h2> <h2>[[config.heading]]</h2>
<div class='root'> <div class="root">
<div id="card"></div> <div id="card"></div>
<template is='dom-if' if='[[showConfig]]'> <template is="dom-if" if="[[showConfig]]">
<pre>[[_trim(config.config)]]</pre> <pre>[[_trim(config.config)]]</pre>
</template> </template>
</div> </div>

View File

@ -25,18 +25,18 @@ class DemoCards extends PolymerElement {
} }
</style> </style>
<app-toolbar> <app-toolbar>
<div class='filters'> <div class="filters">
<paper-toggle-button <paper-toggle-button checked="{{_showConfig}}"
checked='{{_showConfig}}' >Show config</paper-toggle-button
>Show config</paper-toggle-button> >
</div> </div>
</app-toolbar> </app-toolbar>
<div class='cards'> <div class="cards">
<template is='dom-repeat' items='[[configs]]'> <template is="dom-repeat" items="[[configs]]">
<demo-card <demo-card
config='[[item]]' config="[[item]]"
show-config='[[_showConfig]]' show-config="[[_showConfig]]"
hass='[[hass]]' hass="[[hass]]"
></demo-card> ></demo-card>
</template> </template>
</div> </div>

View File

@ -50,11 +50,11 @@ class DemoMoreInfo extends PolymerElement {
></state-card-content> ></state-card-content>
<more-info-content <more-info-content
hass='[[hass]]' hass="[[hass]]"
state-obj='[[_stateObj]]' state-obj="[[_stateObj]]"
></more-info-content> ></more-info-content>
</ha-card> </ha-card>
<template is='dom-if' if='[[showConfig]]'> <template is="dom-if" if="[[showConfig]]">
<pre>[[_jsonEntity(_stateObj)]]</pre> <pre>[[_jsonEntity(_stateObj)]]</pre>
</template> </template>
`; `;

View File

@ -25,18 +25,18 @@ class DemoMoreInfos extends PolymerElement {
} }
</style> </style>
<app-toolbar> <app-toolbar>
<div class='filters'> <div class="filters">
<paper-toggle-button <paper-toggle-button checked="{{_showConfig}}"
checked='{{_showConfig}}' >Show entity</paper-toggle-button
>Show entity</paper-toggle-button> >
</div> </div>
</app-toolbar> </app-toolbar>
<div class='cards'> <div class="cards">
<template is='dom-repeat' items='[[entities]]'> <template is="dom-repeat" items="[[entities]]">
<demo-more-info <demo-more-info
entity-id='[[item]]' entity-id="[[item]]"
show-config='[[_showConfig]]' show-config="[[_showConfig]]"
hass='[[hass]]' hass="[[hass]]"
></demo-more-info> ></demo-more-info>
</template> </template>
</div> </div>

View File

@ -29,6 +29,12 @@ export default (elements, { initialStates = {} } = {}) => {
resources: demoResources, resources: demoResources,
states: initialStates, states: initialStates,
themes: {}, themes: {},
connection: {
subscribeEvents: async (callback, event) => {
console.log("subscribeEvents", event);
return () => console.log("unsubscribeEvents", event);
},
},
// Mock properties // Mock properties
mockEntities: entities, mockEntities: entities,

View File

@ -51,7 +51,11 @@ const CONFIGS = [
class DemoAlarmPanelEntity extends PolymerElement { class DemoAlarmPanelEntity extends PolymerElement {
static get template() { static get template() {
return html` return html`
<demo-cards id='demos' hass='[[hass]]' configs="[[_configs]]"></demo-cards> <demo-cards
id="demos"
hass="[[hass]]"
configs="[[_configs]]"
></demo-cards>
`; `;
} }

View File

@ -57,8 +57,8 @@ class DemoConditional extends PolymerElement {
static get template() { static get template() {
return html` return html`
<demo-cards <demo-cards
id='demos' id="demos"
hass='[[hass]]' hass="[[hass]]"
configs="[[_configs]]" configs="[[_configs]]"
></demo-cards> ></demo-cards>
`; `;

View File

@ -175,10 +175,7 @@ const CONFIGS = [
class DemoEntities extends PolymerElement { class DemoEntities extends PolymerElement {
static get template() { static get template() {
return html` return html`
<demo-cards <demo-cards id="demos" configs="[[_configs]]"></demo-cards>
id='demos'
configs="[[_configs]]"
></demo-cards>
`; `;
} }

View File

@ -71,7 +71,11 @@ const CONFIGS = [
class DemoEntityButtonEntity extends PolymerElement { class DemoEntityButtonEntity extends PolymerElement {
static get template() { static get template() {
return html` return html`
<demo-cards id='demos' hass='[[hass]]' configs="[[_configs]]"></demo-cards> <demo-cards
id="demos"
hass="[[hass]]"
configs="[[_configs]]"
></demo-cards>
`; `;
} }

View File

@ -92,10 +92,7 @@ const CONFIGS = [
class DemoFilter extends PolymerElement { class DemoFilter extends PolymerElement {
static get template() { static get template() {
return html` return html`
<demo-cards <demo-cards id="demos" configs="[[_configs]]"></demo-cards>
id='demos'
configs="[[_configs]]"
></demo-cards>
`; `;
} }

View File

@ -217,10 +217,7 @@ const CONFIGS = [
class DemoPicEntity extends PolymerElement { class DemoPicEntity extends PolymerElement {
static get template() { static get template() {
return html` return html`
<demo-cards <demo-cards id="demos" configs="[[_configs]]"></demo-cards>
id='demos'
configs="[[_configs]]"
></demo-cards>
`; `;
} }

View File

@ -25,7 +25,7 @@ const CONFIGS = [
class DemoLightEntity extends PolymerElement { class DemoLightEntity extends PolymerElement {
static get template() { static get template() {
return html` return html`
<demo-cards id='demos' configs="[[_configs]]"></demo-cards> <demo-cards id="demos" configs="[[_configs]]"></demo-cards>
`; `;
} }

View File

@ -78,8 +78,8 @@ class DemoHuiMediaPlayerRows extends PolymerElement {
static get template() { static get template() {
return html` return html`
<demo-cards <demo-cards
id='demos' id="demos"
hass='[[hass]]' hass="[[hass]]"
configs="[[_configs]]" configs="[[_configs]]"
></demo-cards> ></demo-cards>
`; `;

View File

@ -80,10 +80,7 @@ const CONFIGS = [
class DemoPicElements extends PolymerElement { class DemoPicElements extends PolymerElement {
static get template() { static get template() {
return html` return html`
<demo-cards <demo-cards id="demos" configs="[[_configs]]"></demo-cards>
id='demos'
configs="[[_configs]]"
></demo-cards>
`; `;
} }

View File

@ -0,0 +1,52 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import provideHass from "../data/provide_hass";
import "../components/demo-cards";
const CONFIGS = [
{
heading: "List example",
config: `
- type: shopping-list
`,
},
{
heading: "List with title example",
config: `
- type: shopping-list
title: Shopping List
`,
},
];
class DemoShoppingListEntity extends PolymerElement {
static get template() {
return html`
<demo-cards id="demos" configs="[[_configs]]"></demo-cards>
`;
}
static get properties() {
return {
_configs: {
type: Object,
value: CONFIGS,
},
};
}
ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.mockAPI("shopping_list", () => [
{ name: "list", id: 1, complete: false },
{ name: "all", id: 2, complete: false },
{ name: "the", id: 3, complete: false },
{ name: "things", id: 4, complete: true },
]);
}
}
customElements.define("demo-hui-shopping-list-card", DemoShoppingListEntity);

View File

@ -91,10 +91,7 @@ const CONFIGS = [
class DemoStack extends PolymerElement { class DemoStack extends PolymerElement {
static get template() { static get template() {
return html` return html`
<demo-cards <demo-cards id="demos" configs="[[_configs]]"></demo-cards>
id='demos'
configs="[[_configs]]"
></demo-cards>
`; `;
} }

View File

@ -62,10 +62,7 @@ const CONFIGS = [
class DemoThermostatEntity extends PolymerElement { class DemoThermostatEntity extends PolymerElement {
static get template() { static get template() {
return html` return html`
<demo-cards <demo-cards id="demos" configs="[[_configs]]"></demo-cards>
id='demos'
configs="[[_configs]]"
></demo-cards>
`; `;
} }

View File

@ -34,8 +34,8 @@ class DemoMoreInfoLight extends PolymerElement {
static get template() { static get template() {
return html` return html`
<demo-more-infos <demo-more-infos
hass='[[hass]]' hass="[[hass]]"
entities='[[_entities]]' entities="[[_entities]]"
></demo-more-infos> ></demo-more-infos>
`; `;
} }

View File

@ -111,4 +111,5 @@ gulp.task("gen-icons", ["gen-icons-hass", "gen-icons-mdi"], () => {});
module.exports = { module.exports = {
findIcons, findIcons,
generateIconset, generateIconset,
genMDIIcons,
}; };

View File

@ -11,4 +11,4 @@ OUTPUT_DIR=build
rm -rf $OUTPUT_DIR rm -rf $OUTPUT_DIR
node script/gen-icons.js node script/gen-icons.js
NODE_ENV=production ../node_modules/.bin/webpack -p --config webpack.config.js NODE_ENV=production CI=false ../node_modules/.bin/webpack -p --config webpack.config.js

View File

@ -4,6 +4,8 @@
# Stop on errors # Stop on errors
set -e set -e
cd "$(dirname "$0")/.."
OUTPUT_DIR=build OUTPUT_DIR=build
rm -rf $OUTPUT_DIR rm -rf $OUTPUT_DIR

View File

@ -1,6 +1,10 @@
#!/usr/bin/env node #!/usr/bin/env node
const fs = require("fs"); const fs = require("fs");
const { findIcons, generateIconset } = require("../../gulp/tasks/gen-icons.js"); const {
findIcons,
generateIconset,
genMDIIcons,
} = require("../../gulp/tasks/gen-icons.js");
const MENU_BUTTON_ICON = "menu"; const MENU_BUTTON_ICON = "menu";
@ -9,4 +13,5 @@ function genHassioIcons() {
fs.writeFileSync("./hassio-icons.html", generateIconset("hassio", iconNames)); fs.writeFileSync("./hassio-icons.html", generateIconset("hassio", iconNames));
} }
genMDIIcons();
genHassioIcons(); genHassioIcons();

View File

@ -13,6 +13,9 @@ class HassioAddonRepository extends NavigateMixin(PolymerElement) {
paper-card { paper-card {
cursor: pointer; cursor: pointer;
} }
.not_available {
opacity: 0.6;
}
a.repo { a.repo {
display: block; display: block;
color: var(--primary-text-color); color: var(--primary-text-color);
@ -24,19 +27,34 @@ class HassioAddonRepository extends NavigateMixin(PolymerElement) {
[[repo.name]] [[repo.name]]
<div class="description"> <div class="description">
Maintained by [[repo.maintainer]] Maintained by [[repo.maintainer]]
<a class="repo" href="[[repo.url]]" target="_blank">[[repo.url]]</a> <a class="repo" href="[[repo.url]]" target="_blank"
>[[repo.url]]</a
>
</div> </div>
</div> </div>
<template is="dom-repeat" items="[[addons]]" as="addon" sort="sortAddons"> <template
<paper-card on-click="addonTapped"> is="dom-repeat"
items="[[addons]]"
as="addon"
sort="sortAddons"
>
<paper-card class$="[[computeClass(addon)]]" on-click="addonTapped">
<div class="card-content"> <div class="card-content">
<hassio-card-content hass="[[hass]]" title="[[addon.name]]" description="[[addon.description]]" icon="[[computeIcon(addon)]]" icon-title="[[computeIconTitle(addon)]]" icon-class="[[computeIconClass(addon)]]"></hassio-card-content> <hassio-card-content
hass="[[hass]]"
title="[[addon.name]]"
description="[[addon.description]]"
available="[[addon.available]]"
icon="[[computeIcon(addon)]]"
icon-title="[[computeIconTitle(addon)]]"
icon-class="[[computeIconClass(addon)]]"
></hassio-card-content>
</div> </div>
</paper-card> </paper-card>
</template> </template>
</div> </div>
</template> </template>
`; `;
} }
static get properties() { static get properties() {
@ -62,13 +80,19 @@ class HassioAddonRepository extends NavigateMixin(PolymerElement) {
return addon.installed !== addon.version return addon.installed !== addon.version
? "New version available" ? "New version available"
: "Add-on is installed"; : "Add-on is installed";
return "Add-on is not installed"; return addon.available
? "Add-on is not installed"
: "Add-on is not available on your system";
} }
computeIconClass(addon) { computeIconClass(addon) {
if (addon.installed) if (addon.installed)
return addon.installed !== addon.version ? "update" : "installed"; return addon.installed !== addon.version ? "update" : "installed";
return ""; return !addon.available ? "not_available" : "";
}
computeClass(addon) {
return !addon.available ? "not_available" : "";
} }
addonTapped(ev) { addonTapped(ev) {

View File

@ -12,12 +12,19 @@ class HassioAddonStore extends PolymerElement {
margin-top: 24px; margin-top: 24px;
} }
</style> </style>
<hassio-repositories-editor hass="[[hass]]" repos="[[repos]]"></hassio-repositories-editor> <hassio-repositories-editor
hass="[[hass]]"
repos="[[repos]]"
></hassio-repositories-editor>
<template is="dom-repeat" items="[[repos]]" as="repo" sort="sortRepos"> <template is="dom-repeat" items="[[repos]]" as="repo" sort="sortRepos">
<hassio-addon-repository hass="[[hass]]" repo="[[repo]]" addons="[[computeAddons(repo.slug)]]"></hassio-addon-repository> <hassio-addon-repository
hass="[[hass]]"
repo="[[repo]]"
addons="[[computeAddons(repo.slug)]]"
></hassio-addon-repository>
</template> </template>
`; `;
} }
static get properties() { static get properties() {

View File

@ -32,27 +32,52 @@ class HassioRepositoriesEditor extends PolymerElement {
Configure which add-on repositories to fetch data from: Configure which add-on repositories to fetch data from:
</div> </div>
</div> </div>
<template id="list" is="dom-repeat" items="[[repoList]]" as="repo" sort="sortRepos"> <template
id="list"
is="dom-repeat"
items="[[repoList]]"
as="repo"
sort="sortRepos"
>
<paper-card> <paper-card>
<div class="card-content"> <div class="card-content">
<hassio-card-content hass="[[hass]]" title="[[repo.name]]" description="[[repo.url]]" icon="hassio:github-circle"></hassio-card-content> <hassio-card-content
hass="[[hass]]"
title="[[repo.name]]"
description="[[repo.url]]"
icon="hassio:github-circle"
></hassio-card-content>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-call-api-button hass="[[hass]]" path="hassio/supervisor/options" data="[[computeRemoveRepoData(repoList, repo.url)]]" class="warning">Remove</ha-call-api-button> <ha-call-api-button
hass="[[hass]]"
path="hassio/supervisor/options"
data="[[computeRemoveRepoData(repoList, repo.url)]]"
class="warning"
>Remove</ha-call-api-button
>
</div> </div>
</paper-card> </paper-card>
</template> </template>
<paper-card> <paper-card>
<div class="card-content add"> <div class="card-content add">
<iron-icon icon="hassio:github-circle"></iron-icon> <iron-icon icon="hassio:github-circle"></iron-icon>
<paper-input label="Add new repository by URL" value="{{repoUrl}}"></paper-input> <paper-input
label="Add new repository by URL"
value="{{repoUrl}}"
></paper-input>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-call-api-button hass="[[hass]]" path="hassio/supervisor/options" data="[[computeAddRepoData(repoList, repoUrl)]]">Add</ha-call-api-button> <ha-call-api-button
hass="[[hass]]"
path="hassio/supervisor/options"
data="[[computeAddRepoData(repoList, repoUrl)]]"
>Add</ha-call-api-button
>
</div> </div>
</paper-card> </paper-card>
</div> </div>
`; `;
} }
static get properties() { static get properties() {

View File

@ -38,16 +38,28 @@ class HassioAddonAudio extends EventsMixin(PolymerElement) {
</template> </template>
<paper-dropdown-menu label="Input"> <paper-dropdown-menu label="Input">
<paper-listbox slot="dropdown-content" attr-for-selected="device" selected="{{selectedInput}}"> <paper-listbox
slot="dropdown-content"
attr-for-selected="device"
selected="{{selectedInput}}"
>
<template is="dom-repeat" items="[[inputDevices]]"> <template is="dom-repeat" items="[[inputDevices]]">
<paper-item device\$="[[item.device]]">[[item.name]]</paper-item> <paper-item device\$="[[item.device]]"
>[[item.name]]</paper-item
>
</template> </template>
</paper-listbox> </paper-listbox>
</paper-dropdown-menu> </paper-dropdown-menu>
<paper-dropdown-menu label="Output"> <paper-dropdown-menu label="Output">
<paper-listbox slot="dropdown-content" attr-for-selected="device" selected="{{selectedOutput}}"> <paper-listbox
slot="dropdown-content"
attr-for-selected="device"
selected="{{selectedOutput}}"
>
<template is="dom-repeat" items="[[outputDevices]]"> <template is="dom-repeat" items="[[outputDevices]]">
<paper-item device\$="[[item.device]]">[[item.name]]</paper-item> <paper-item device\$="[[item.device]]"
>[[item.name]]</paper-item
>
</template> </template>
</paper-listbox> </paper-listbox>
</paper-dropdown-menu> </paper-dropdown-menu>
@ -56,7 +68,7 @@ class HassioAddonAudio extends EventsMixin(PolymerElement) {
<paper-button on-click="_saveSettings">Save</paper-button> <paper-button on-click="_saveSettings">Save</paper-button>
</div> </div>
</paper-card> </paper-card>
`; `;
} }
static get properties() { static get properties() {

View File

@ -37,14 +37,25 @@ class HassioAddonConfig extends PolymerElement {
<template is="dom-if" if="[[error]]"> <template is="dom-if" if="[[error]]">
<div class="errors">[[error]]</div> <div class="errors">[[error]]</div>
</template> </template>
<iron-autogrow-textarea id="config" value="{{config}}"></iron-autogrow-textarea> <iron-autogrow-textarea
id="config"
value="{{config}}"
></iron-autogrow-textarea>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-call-api-button class="warning" hass="[[hass]]" path="hassio/addons/[[addonSlug]]/options" data="[[resetData]]">Reset to defaults</ha-call-api-button> <ha-call-api-button
<paper-button on-click="saveTapped" disabled="[[!configParsed]]">Save</paper-button> class="warning"
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/options"
data="[[resetData]]"
>Reset to defaults</ha-call-api-button
>
<paper-button on-click="saveTapped" disabled="[[!configParsed]]"
>Save</paper-button
>
</div> </div>
</paper-card> </paper-card>
`; `;
} }
static get properties() { static get properties() {

View File

@ -5,13 +5,62 @@ import "@polymer/paper-toggle-button/paper-toggle-button";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/buttons/ha-call-api-button"; import "../../../src/components/ha-label-badge";
import "../../../src/components/ha-markdown"; import "../../../src/components/ha-markdown";
import "../../../src/components/buttons/ha-call-api-button";
import "../../../src/resources/ha-style"; import "../../../src/resources/ha-style";
import EventsMixin from "../../../src/mixins/events-mixin"; import EventsMixin from "../../../src/mixins/events-mixin";
import "../components/hassio-card-content"; import "../components/hassio-card-content";
const PERMIS_DESC = {
rating: {
title: "Addon Security Rating",
description:
"Hass.io provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an addon requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 6. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 6 is the highest score (considered the most secure and lowest risk).",
},
host_network: {
title: "Host Network",
description:
"Add-ons usually run in their own isolated network layer, which prevents them from accessing the network of the host operating system. In some cases, this network isolation can limit add-ons in providing their services and therefore, the isolation can be lifted by the add-on author, giving the addon full access to the network capabilities of the host machine. This gives the addon more networking capabilities but lowers the security, hence, the security rating of the add-on will be lowered when this option is used by the addon.",
},
homeassistant_api: {
title: "Home Assistant API Access",
description:
"This add-on is allowed to access your running Home Assistant instance directly via the Home Assistant API. This mode handles authentication for the addon as well, which enables an addon to interact with Home Assistant without the need for additional authentication tokens.",
},
full_access: {
title: "Full Hardware Access",
description:
"This addon is given full access to the hardware of your system, by request of the addon author. Access is comparable to the privileged mode in Docker. Since this opens up possible security risks, this feature impacts the addon security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the addon manually. Only disable the protection mode if you know, need AND trust the source of this addon.",
},
hassio_api: {
title: "Hass.io API Access",
description:
"The addon was given access to the Hass.io API, by request of the addon author. By default, the addon can access general version information of your system. When the addon requests 'manager' or 'admin' level access to the API, it will gain access to control multiple parts of your Hass.io system. This permission is indicated by this badge and will impact the security score of the addon negatively.",
},
docker_api: {
title: "Full Docker Access",
description:
"The addon author has requested the addon to have management access to the Docker instance running on your system. This mode gives the addon full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the addon security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the addon manually. Only disable the protection mode if you know, need AND trust the source of this addon.",
},
host_pid: {
title: "Host Processes Namespace",
description:
"Usually, the processes the addon runs, are isolated from all other system processes. The addon author has requested the addon to have access to the system processes running on the host system instance, and allow the addon to spawn processes on the host system as well. This mode gives the addon full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the addon security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the addon manually. Only disable the protection mode if you know, need AND trust the source of this addon.",
},
apparmor: {
title: "AppArmor",
description:
"AppArmor ('Application Armor') is a Linux kernel security module that restricts addons capabilities like network access, raw socket access, and permission to read, write, or execute specific files.\n\nAddon authors can provide their security profiles, optimized for the addon, or request it to be disabled. If AppArmor is disabled, it will raise security risks and therefore, has a negative impact on the security score of the addon.",
},
auth_api: {
title: "Home Assistant Authentication",
description:
"An addon can authenticate users against Home Assistant, allowing add-ons to give users the possibility to log into applications running inside add-ons, using their Home Assistant username/password. This badge indicates if the add-on author requests this capability.",
},
};
class HassioAddonInfo extends EventsMixin(PolymerElement) { class HassioAddonInfo extends EventsMixin(PolymerElement) {
static get template() { static get template() {
return html` return html`
@ -23,6 +72,17 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
} }
paper-card.warning {
background-color: var(--google-red-500);
color: white;
--paper-card-header-color: white;
}
paper-card.warning paper-button {
color: white !important;
}
.warning {
color: var(--google-red-500);
}
.addon-header { .addon-header {
@apply --paper-font-headline; @apply --paper-font-headline;
} }
@ -42,7 +102,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
margin: 16px 0; margin: 16px 0;
display: block; display: block;
} }
.state div{ .state div {
width: 150px; width: 150px;
display: inline-block; display: inline-block;
} }
@ -65,14 +125,49 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
ha-markdown img { ha-markdown img {
max-width: 100%; max-width: 100%;
} }
.red {
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
}
.blue {
--ha-label-badge-color: var(--label-badge-blue, #039be5);
}
.green {
--ha-label-badge-color: var(--label-badge-green, #0da035);
}
.yellow {
--ha-label-badge-color: var(--label-badge-yellow, #f4b400);
}
.security {
margin-bottom: 16px;
}
.security h3 {
margin-bottom: 8px;
font-weight: normal;
}
.security ha-label-badge {
cursor: pointer;
margin-right: 4px;
--iron-icon-height: 45px;
}
</style> </style>
<template is="dom-if" if="[[computeUpdateAvailable(addon)]]"> <template is="dom-if" if="[[computeUpdateAvailable(addon)]]">
<paper-card heading="Update available! 🎉"> <paper-card heading="Update available! 🎉">
<div class="card-content"> <div class="card-content">
<hassio-card-content hass="[[hass]]" title="[[addon.name]] [[addon.last_version]] is available" description="You are currently running version [[addon.version]]" icon="hassio:arrow-up-bold-circle" icon-class="update"></hassio-card-content> <hassio-card-content
hass="[[hass]]"
title="[[addon.name]] [[addon.last_version]] is available"
description="You are currently running version [[addon.version]]"
icon="hassio:arrow-up-bold-circle"
icon-class="update"
></hassio-card-content>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-call-api-button hass="[[hass]]" path="hassio/addons/[[addonSlug]]/update">Update</ha-call-api-button> <ha-call-api-button
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/update"
>Update</ha-call-api-button
>
<template is="dom-if" if="[[addon.changelog]]"> <template is="dom-if" if="[[addon.changelog]]">
<paper-button on-click="openChangelog">Changelog</paper-button> <paper-button on-click="openChangelog">Changelog</paper-button>
</template> </template>
@ -82,15 +177,24 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
<paper-card> <paper-card>
<div class="card-content"> <div class="card-content">
<div class="addon-header">[[addon.name]] <div class="addon-header">
[[addon.name]]
<div class="addon-version light-color"> <div class="addon-version light-color">
<template is="dom-if" if="[[addon.version]]"> <template is="dom-if" if="[[addon.version]]">
[[addon.version]] [[addon.version]]
<template is="dom-if" if="[[isRunning]]"> <template is="dom-if" if="[[isRunning]]">
<iron-icon title="Add-on is running" class="running" icon="hassio:circle"></iron-icon> <iron-icon
title="Add-on is running"
class="running"
icon="hassio:circle"
></iron-icon>
</template> </template>
<template is="dom-if" if="[[!isRunning]]"> <template is="dom-if" if="[[!isRunning]]">
<iron-icon title="Add-on is stopped" class="stopped" icon="hassio:circle"></iron-icon> <iron-icon
title="Add-on is stopped"
class="stopped"
icon="hassio:circle"
></iron-icon>
</template> </template>
</template> </template>
<template is="dom-if" if="[[!addon.version]]"> <template is="dom-if" if="[[!addon.version]]">
@ -99,44 +203,195 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
</div> </div>
</div> </div>
<div class="description light-color"> <div class="description light-color">
[[addon.description]].<br> [[addon.description]].<br />
Visit <a href="[[addon.url]]" target="_blank">[[addon.name]] page</a> for details. Visit
<a href="[[addon.url]]" target="_blank">[[addon.name]] page</a> for
details.
</div> </div>
<template is="dom-if" if="[[addon.logo]]"> <template is="dom-if" if="[[addon.logo]]">
<a href="[[addon.url]]" target="_blank" class="logo"> <a href="[[addon.url]]" target="_blank" class="logo">
<img src="/api/hassio/addons/[[addonSlug]]/logo"> <img src="/api/hassio/addons/[[addonSlug]]/logo" />
</a> </a>
</template> </template>
<template is="dom-if" if="[[!addon.protected]]">
<paper-card heading="Warning: Protection mode is disabled!" class="warning">
<div class="card-content">
Protection mode on this addon is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this addon.
</div>
<div class="card-actions">
<paper-button on-click="protectionToggled">Enable Protection mode</paper-button>
</div>
</div>
</paper-card>
</template>
<div class="security">
<h3>Addon Security Rating</h3>
<div class="description light-color">
Hass.io provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an addon requires on your system, the lower the score, thus raising the possible security risks.
</div>
<ha-label-badge
class$="[[computeSecurityClassName(addon.rating)]]"
on-click="showMoreInfo"
id="rating"
value="[[addon.rating]]"
label="rating"
description=""
></ha-label-badge>
<template is="dom-if" if="[[addon.host_network]]">
<ha-label-badge
on-click="showMoreInfo"
id="host_network"
icon="hassio:network"
label="host"
description=""
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.full_access]]">
<ha-label-badge
on-click="showMoreInfo"
id="full_access"
icon="hassio:chip"
label="hardware"
description=""
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.homeassistant_api]]">
<ha-label-badge
on-click="showMoreInfo"
id="homeassistant_api"
icon="hassio:home-assistant"
label="hass"
description=""
></ha-label-badge>
</template>
<template is="dom-if" if="[[computeHassioApi(addon)]]">
<ha-label-badge
on-click="showMoreInfo"
id="hassio_api"
icon="hassio:home-assistant"
label="hassio"
description="[[addon.hassio_role]]"
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.docker_api]]">
<ha-label-badge
on-click="showMoreInfo"
id="docker_api"
icon="hassio:docker"
label="docker"
description=""
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.host_pid]]">
<ha-label-badge
on-click="showMoreInfo"
id="host_pid"
icon="hassio:pound"
label="host pid"
description=""
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.apparmor]]">
<ha-label-badge
on-click="showMoreInfo"
class$="[[computeApparmorClassName(addon.apparmor)]]"
id="apparmor"
icon="hassio:shield"
label="apparmor"
description="[[addon.apparmor]]"
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.auth_api]]">
<ha-label-badge
on-click="showMoreInfo"
id="auth_api"
icon="hassio:key"
label="auth"
description=""
></ha-label-badge>
</template>
</div>
<template is="dom-if" if="[[addon.version]]"> <template is="dom-if" if="[[addon.version]]">
<div class="state"> <div class="state">
<div>Start on boot</div> <div>Start on boot</div>
<paper-toggle-button on-change="startOnBootToggled" checked="[[computeStartOnBoot(addon.boot)]]"></paper-toggle-button> <paper-toggle-button
on-change="startOnBootToggled"
checked="[[computeStartOnBoot(addon.boot)]]"
></paper-toggle-button>
</div> </div>
<div class="state"> <div class="state">
<div>Auto update</div> <div>Auto update</div>
<paper-toggle-button on-change="autoUpdateToggled" checked="[[addon.auto_update]]"></paper-toggle-button> <paper-toggle-button
on-change="autoUpdateToggled"
checked="[[addon.auto_update]]"
></paper-toggle-button>
</div>
<div class="state">
<div>Protection mode</div>
<paper-toggle-button
on-change="protectionToggled"
checked="[[addon.protected]]"
></paper-toggle-button>
</div> </div>
</template> </template>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<template is="dom-if" if="[[addon.version]]"> <template is="dom-if" if="[[addon.version]]">
<paper-button class="warning" on-click="_unistallClicked">Uninstall</paper-button> <paper-button class="warning" on-click="_unistallClicked"
>Uninstall</paper-button
>
<template is="dom-if" if="[[addon.build]]"> <template is="dom-if" if="[[addon.build]]">
<ha-call-api-button class="warning" hass="[[hass]]" path="hassio/addons/[[addonSlug]]/rebuild">Rebuild</ha-call-api-button> <ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/rebuild"
>Rebuild</ha-call-api-button
>
</template> </template>
<template is="dom-if" if="[[isRunning]]"> <template is="dom-if" if="[[isRunning]]">
<ha-call-api-button class="warning" hass="[[hass]]" path="hassio/addons/[[addonSlug]]/restart">Restart</ha-call-api-button> <ha-call-api-button
<ha-call-api-button class="warning" hass="[[hass]]" path="hassio/addons/[[addonSlug]]/stop">Stop</ha-call-api-button> class="warning"
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/restart"
>Restart</ha-call-api-button
>
<ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/stop"
>Stop</ha-call-api-button
>
</template> </template>
<template is="dom-if" if="[[!isRunning]]"> <template is="dom-if" if="[[!isRunning]]">
<ha-call-api-button hass="[[hass]]" path="hassio/addons/[[addonSlug]]/start">Start</ha-call-api-button> <ha-call-api-button
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/start"
>Start</ha-call-api-button
>
</template> </template>
<template is="dom-if" if="[[computeShowWebUI(addon.webui, isRunning)]]"> <template
<a href="[[pathWebui(addon.webui)]]" tabindex="-1" target="_blank" class="right"><paper-button>Open web UI</paper-button></a> is="dom-if"
if="[[computeShowWebUI(addon.webui, isRunning)]]"
>
<a
href="[[pathWebui(addon.webui)]]"
tabindex="-1"
target="_blank"
class="right"
><paper-button>Open web UI</paper-button></a
>
</template> </template>
</template> </template>
<template is="dom-if" if="[[!addon.version]]"> <template is="dom-if" if="[[!addon.version]]">
<ha-call-api-button hass="[[hass]]" path="hassio/addons/[[addonSlug]]/install">Install</ha-call-api-button> <template is="dom-if" if="[[!addon.available]]">
<p class="warning">This addon is not available on your system.</p>
</template>
<ha-call-api-button
disabled="[[!addon.available]]"
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/install"
>Install</ha-call-api-button
>
</template> </template>
</div> </div>
</paper-card> </paper-card>
@ -147,7 +402,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
</div> </div>
</paper-card> </paper-card>
</template> </template>
`; `;
} }
static get properties() { static get properties() {
@ -155,10 +410,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
hass: Object, hass: Object,
addon: Object, addon: Object,
addonSlug: String, addonSlug: String,
isRunning: { isRunning: { type: Boolean, computed: "computeIsRunning(addon)" },
type: Boolean,
computed: "computeIsRunning(addon)",
},
}; };
} }
@ -175,6 +427,23 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
); );
} }
computeHassioApi(addon) {
return (
addon.hassio_api &&
(addon.hassio_role === "manager" || addon.hassio_role === "admin")
);
}
computeApparmorClassName(apparmor) {
if (apparmor === "profile") {
return "green";
}
if (apparmor === "disable") {
return "red";
}
return "";
}
pathWebui(webui) { pathWebui(webui) {
return webui && webui.replace("[HOST]", document.location.hostname); return webui && webui.replace("[HOST]", document.location.hostname);
} }
@ -187,6 +456,16 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
return state === "auto"; return state === "auto";
} }
computeSecurityClassName(rating) {
if (rating > 4) {
return "green";
}
if (rating > 2) {
return "yellow";
}
return "red";
}
startOnBootToggled() { startOnBootToggled() {
const data = { boot: this.addon.boot === "auto" ? "manual" : "auto" }; const data = { boot: this.addon.boot === "auto" ? "manual" : "auto" };
this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/options`, data); this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/options`, data);
@ -197,6 +476,20 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/options`, data); this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/options`, data);
} }
protectionToggled() {
const data = { protected: !this.addon.protected };
this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/security`, data);
this.set("addon.protected", !this.addon.protected);
}
showMoreInfo(e) {
const id = e.target.getAttribute("id");
this.fire("hassio-markdown-dialog", {
title: PERMIS_DESC[id].title,
content: PERMIS_DESC[id].description,
});
}
openChangelog() { openChangelog() {
this.hass this.hass
.callApi("get", `hassio/addons/${this.addonSlug}/changelog`) .callApi("get", `hassio/addons/${this.addonSlug}/changelog`)

View File

@ -18,14 +18,12 @@ class HassioAddonLogs extends PolymerElement {
} }
</style> </style>
<paper-card heading="Log"> <paper-card heading="Log">
<div class="card-content"> <div class="card-content"><pre>[[log]]</pre></div>
<pre>[[log]]</pre>
</div>
<div class="card-actions"> <div class="card-actions">
<paper-button on-click="refresh">Refresh</paper-button> <paper-button on-click="refresh">Refresh</paper-button>
</div> </div>
</paper-card> </paper-card>
`; `;
} }
static get properties() { static get properties() {

View File

@ -33,28 +33,37 @@ class HassioAddonNetwork extends EventsMixin(PolymerElement) {
</template> </template>
<table> <table>
<tbody><tr> <tbody>
<tr>
<th>Container</th> <th>Container</th>
<th>Host</th> <th>Host</th>
</tr> </tr>
<template is="dom-repeat" items="[[config]]"> <template is="dom-repeat" items="[[config]]">
<tr> <tr>
<td>[[item.container]]</td>
<td> <td>
[[item.container]] <paper-input
</td> value="{{item.host}}"
<td> no-label-float=""
<paper-input value="{{item.host}}" no-label-float=""></paper-input> ></paper-input>
</td> </td>
</tr> </tr>
</template> </template>
</tbody></table> </tbody>
</table>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-call-api-button class="warning" hass="[[hass]]" path="hassio/addons/[[addonSlug]]/options" data="[[resetData]]">Reset to defaults</ha-call-api-button> <ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/options"
data="[[resetData]]"
>Reset to defaults</ha-call-api-button
>
<paper-button on-click="saveTapped">Save</paper-button> <paper-button on-click="saveTapped">Save</paper-button>
</div> </div>
</paper-card> </paper-card>
`; `;
} }
static get properties() { static get properties() {

View File

@ -51,36 +51,69 @@ class HassioAddonView extends PolymerElement {
} }
} }
</style> </style>
<app-route route="[[route]]" pattern="/addon/:slug" data="{{routeData}}" active="{{routeMatches}}"></app-route> <app-route
route="[[route]]"
pattern="/addon/:slug"
data="{{routeData}}"
active="{{routeMatches}}"
></app-route>
<app-header-layout has-scrolling-region=""> <app-header-layout has-scrolling-region="">
<app-header fixed="" slot="header"> <app-header fixed="" slot="header">
<app-toolbar> <app-toolbar>
<ha-menu-button hassio narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button> <ha-menu-button
<paper-icon-button icon="hassio:arrow-left" on-click="backTapped"></paper-icon-button> hassio
narrow="[[narrow]]"
show-menu="[[showMenu]]"
></ha-menu-button>
<paper-icon-button
icon="hassio:arrow-left"
on-click="backTapped"
></paper-icon-button>
<div main-title="">Hass.io: add-on details</div> <div main-title="">Hass.io: add-on details</div>
</app-toolbar> </app-toolbar>
</app-header> </app-header>
<div class="content"> <div class="content">
<hassio-addon-info hass="[[hass]]" addon="[[addon]]" addon-slug="[[routeData.slug]]"></hassio-addon-info> <hassio-addon-info
hass="[[hass]]"
addon="[[addon]]"
addon-slug="[[routeData.slug]]"
></hassio-addon-info>
<template is="dom-if" if="[[addon.version]]"> <template is="dom-if" if="[[addon.version]]">
<hassio-addon-config hass="[[hass]]" addon="[[addon]]" addon-slug="[[routeData.slug]]"></hassio-addon-config> <hassio-addon-config
hass="[[hass]]"
addon="[[addon]]"
addon-slug="[[routeData.slug]]"
></hassio-addon-config>
<template is="dom-if" if="[[addon.audio]]"> <template is="dom-if" if="[[addon.audio]]">
<hassio-addon-audio hass="[[hass]]" addon="[[addon]]"></hassio-addon-audio> <hassio-addon-audio
hass="[[hass]]"
addon="[[addon]]"
></hassio-addon-audio>
</template> </template>
<template is="dom-if" if="[[addon.network]]"> <template is="dom-if" if="[[addon.network]]">
<hassio-addon-network hass="[[hass]]" addon="[[addon]]" addon-slug="[[routeData.slug]]"></hassio-addon-network> <hassio-addon-network
hass="[[hass]]"
addon="[[addon]]"
addon-slug="[[routeData.slug]]"
></hassio-addon-network>
</template> </template>
<hassio-addon-logs hass="[[hass]]" addon-slug="[[routeData.slug]]"></hassio-addon-logs> <hassio-addon-logs
hass="[[hass]]"
addon-slug="[[routeData.slug]]"
></hassio-addon-logs>
</template> </template>
</div> </div>
</app-header-layout> </app-header-layout>
<hassio-markdown-dialog title="[[markdownTitle]]" content="[[markdownContent]]"></hassio-markdown-dialog> <hassio-markdown-dialog
`; title="[[markdownTitle]]"
content="[[markdownContent]]"
></hassio-markdown-dialog>
`;
} }
static get properties() { static get properties() {

View File

@ -25,6 +25,9 @@ class HassioCardContent extends PolymerElement {
iron-icon.snapshot { iron-icon.snapshot {
color: var(--paper-item-icon-color); color: var(--paper-item-icon-color);
} }
iron-icon.not_available {
color: var(--google-red-500);
}
.title { .title {
color: var(--primary-text-color); color: var(--primary-text-color);
white-space: nowrap; white-space: nowrap;
@ -42,19 +45,30 @@ class HassioCardContent extends PolymerElement {
display: block; display: block;
} }
</style> </style>
<iron-icon icon="[[icon]]" class\$="[[iconClass]]" title="[[iconTitle]]"></iron-icon> <iron-icon
icon="[[icon]]"
class\$="[[iconClass]]"
title="[[iconTitle]]"
></iron-icon>
<div> <div>
<div class="title">[[title]]</div> <div class="title">[[title]]</div>
<div class="addition"> <div class="addition">
<template is="dom-if" if="[[description]]"> <template is="dom-if" if="[[description]]">
[[description]] [[description]]
</template> </template>
<template is="dom-if" if="[[!available]]">
(Not available)
</template>
<template is="dom-if" if="[[datetime]]"> <template is="dom-if" if="[[datetime]]">
<ha-relative-time hass="[[hass]]" class="addition" datetime="[[datetime]]"></ha-relative-time> <ha-relative-time
hass="[[hass]]"
class="addition"
datetime="[[datetime]]"
></ha-relative-time>
</template> </template>
</div> </div>
</div> </div>
`; `;
} }
static get properties() { static get properties() {
@ -62,6 +76,7 @@ class HassioCardContent extends PolymerElement {
hass: Object, hass: Object,
title: String, title: String,
description: String, description: String,
available: Boolean,
datetime: String, datetime: String,
icon: { icon: {
type: String, type: String,

View File

@ -19,19 +19,34 @@ class HassioAddons extends NavigateMixin(PolymerElement) {
<template is="dom-if" if="[[!addons.length]]"> <template is="dom-if" if="[[!addons.length]]">
<paper-card> <paper-card>
<div class="card-content"> <div class="card-content">
You don't have any add-ons installed yet. Head over to <a href="#" on-click="openStore">the add-on store</a> to get started! You don't have any add-ons installed yet. Head over to
<a href="#" on-click="openStore">the add-on store</a> to get
started!
</div> </div>
</paper-card> </paper-card>
</template> </template>
<template is="dom-repeat" items="[[addons]]" as="addon" sort="sortAddons"> <template
is="dom-repeat"
items="[[addons]]"
as="addon"
sort="sortAddons"
>
<paper-card on-click="addonTapped"> <paper-card on-click="addonTapped">
<div class="card-content"> <div class="card-content">
<hassio-card-content hass="[[hass]]" title="[[addon.name]]" description="[[addon.description]]" icon="[[computeIcon(addon)]]" icon-title="[[computeIconTitle(addon)]]" icon-class="[[computeIconClass(addon)]]"></hassio-card-content> <hassio-card-content
hass="[[hass]]"
title="[[addon.name]]"
description="[[addon.description]]"
available="[[addon.available]]"
icon="[[computeIcon(addon)]]"
icon-title="[[computeIconTitle(addon)]]"
icon-class="[[computeIconClass(addon)]]"
></hassio-card-content>
</div> </div>
</paper-card> </paper-card>
</template> </template>
</div> </div>
`; `;
} }
static get properties() { static get properties() {

View File

@ -14,10 +14,16 @@ class HassioDashboard extends EventsMixin(PolymerElement) {
} }
</style> </style>
<div class="content"> <div class="content">
<hassio-hass-update hass="[[hass]]" hass-info="[[hassInfo]]"></hassio-hass-update> <hassio-hass-update
<hassio-addons hass="[[hass]]" addons="[[supervisorInfo.addons]]"></hassio-addons> hass="[[hass]]"
hass-info="[[hassInfo]]"
></hassio-hass-update>
<hassio-addons
hass="[[hass]]"
addons="[[supervisorInfo.addons]]"
></hassio-addons>
</div> </div>
`; `;
} }
static get properties() { static get properties() {

View File

@ -29,21 +29,41 @@ class HassioHassUpdate extends PolymerElement {
<div class="title">Update available! 🎉</div> <div class="title">Update available! 🎉</div>
<paper-card> <paper-card>
<div class="card-content"> <div class="card-content">
<hassio-card-content hass="[[hass]]" title="Home Assistant [[hassInfo.last_version]] is available" description="You are currently running version [[hassInfo.version]]" icon="hassio:home-assistant" icon-class="hassupdate"></hassio-card-content> <hassio-card-content
hass="[[hass]]"
title="Home Assistant [[hassInfo.last_version]] is available"
description="You are currently running version [[hassInfo.version]]"
icon="hassio:home-assistant"
icon-class="hassupdate"
></hassio-card-content>
<template is="dom-if" if="[[error]]"> <template is="dom-if" if="[[error]]">
<div class="error">Error: [[error]]</div> <div class="error">Error: [[error]]</div>
</template> </template>
<p><a href='https://www.home-assistant.io/latest-release-notes/' target='_blank'>Read the release notes</a></p> <p>
<a
href="https://www.home-assistant.io/latest-release-notes/"
target="_blank"
>Read the release notes</a
>
</p>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-call-api-button hass="[[hass]]" path="hassio/homeassistant/update">Update</ha-call-api-button> <ha-call-api-button
<a href="https://github.com/home-assistant/home-assistant/releases" target="_blank"><paper-button>Release notes</paper-button></a> hass="[[hass]]"
path="hassio/homeassistant/update"
>Update</ha-call-api-button
>
<a
href="https://github.com/home-assistant/home-assistant/releases"
target="_blank"
><paper-button>Release notes</paper-button></a
>
</div> </div>
</paper-card> </paper-card>
</div> </div>
</div> </div>
</template> </template>
`; `;
} }
static get properties() { static get properties() {

View File

@ -8,9 +8,14 @@ class HassioApp extends PolymerElement {
static get template() { static get template() {
return html` return html`
<template is="dom-if" if="[[hass]]"> <template is="dom-if" if="[[hass]]">
<hassio-main hass="[[hass]]" narrow="[[narrow]]" show-menu="[[showMenu]]" route="[[route]]"></hassio-main> <hassio-main
hass="[[hass]]"
narrow="[[narrow]]"
show-menu="[[showMenu]]"
route="[[route]]"
></hassio-main>
</template> </template>
`; `;
} }
static get properties() { static get properties() {

View File

@ -14,22 +14,48 @@ import NavigateMixin from "../../src/mixins/navigate-mixin";
class HassioMain extends EventsMixin(NavigateMixin(PolymerElement)) { class HassioMain extends EventsMixin(NavigateMixin(PolymerElement)) {
static get template() { static get template() {
return html` return html`
<app-route route="[[route]]" pattern="/:page" data="{{routeData}}"></app-route> <app-route
<hassio-data id="data" hass="[[hass]]" supervisor="{{supervisorInfo}}" homeassistant="{{hassInfo}}" host="{{hostInfo}}"></hassio-data> route="[[route]]"
pattern="/:page"
data="{{routeData}}"
></app-route>
<hassio-data
id="data"
hass="[[hass]]"
supervisor="{{supervisorInfo}}"
homeassistant="{{hassInfo}}"
host="{{hostInfo}}"
></hassio-data>
<template is="dom-if" if="[[!loaded]]"> <template is="dom-if" if="[[!loaded]]">
<hass-loading-screen narrow="[[narrow]]" show-menu="[[showMenu]]"></hass-loading-screen> <hass-loading-screen
narrow="[[narrow]]"
show-menu="[[showMenu]]"
></hass-loading-screen>
</template> </template>
<template is="dom-if" if="[[loaded]]"> <template is="dom-if" if="[[loaded]]">
<template is="dom-if" if="[[!equalsAddon(routeData.page)]]"> <template is="dom-if" if="[[!equalsAddon(routeData.page)]]">
<hassio-pages-with-tabs hass="[[hass]]" narrow="[[narrow]]" show-menu="[[showMenu]]" page="[[routeData.page]]" supervisor-info="[[supervisorInfo]]" hass-info="[[hassInfo]]" host-info="[[hostInfo]]"></hassio-pages-with-tabs> <hassio-pages-with-tabs
hass="[[hass]]"
narrow="[[narrow]]"
show-menu="[[showMenu]]"
page="[[routeData.page]]"
supervisor-info="[[supervisorInfo]]"
hass-info="[[hassInfo]]"
host-info="[[hostInfo]]"
></hassio-pages-with-tabs>
</template> </template>
<template is="dom-if" if="[[equalsAddon(routeData.page)]]"> <template is="dom-if" if="[[equalsAddon(routeData.page)]]">
<hassio-addon-view hass="[[hass]]" narrow="[[narrow]]" show-menu="[[showMenu]]" route="[[route]]"></hassio-addon-view> <hassio-addon-view
hass="[[hass]]"
narrow="[[narrow]]"
show-menu="[[showMenu]]"
route="[[route]]"
></hassio-addon-view>
</template> </template>
</template> </template>
`; `;
} }
static get properties() { static get properties() {

View File

@ -52,14 +52,17 @@ class HassioMarkdownDialog extends PolymerElement {
</style> </style>
<paper-dialog id="dialog" with-backdrop=""> <paper-dialog id="dialog" with-backdrop="">
<app-toolbar> <app-toolbar>
<paper-icon-button icon="hassio:close" dialog-dismiss=""></paper-icon-button> <paper-icon-button
icon="hassio:close"
dialog-dismiss=""
></paper-icon-button>
<div main-title="">[[title]]</div> <div main-title="">[[title]]</div>
</app-toolbar> </app-toolbar>
<paper-dialog-scrollable> <paper-dialog-scrollable>
<ha-markdown content="[[content]]"></ha-markdown> <ha-markdown content="[[content]]"></ha-markdown>
</paper-dialog-scrollable> </paper-dialog-scrollable>
</paper-dialog> </paper-dialog>
`; `;
} }
static get properties() { static get properties() {

View File

@ -30,20 +30,32 @@ class HassioPagesWithTabs extends NavigateMixin(PolymerElement) {
} }
paper-tabs { paper-tabs {
margin-left: 12px; margin-left: 12px;
--paper-tabs-selection-bar-color: #FFF; --paper-tabs-selection-bar-color: #fff;
text-transform: uppercase; text-transform: uppercase;
} }
</style> </style>
<app-header-layout id="layout" has-scrolling-region> <app-header-layout id="layout" has-scrolling-region>
<app-header fixed slot="header"> <app-header fixed slot="header">
<app-toolbar> <app-toolbar>
<ha-menu-button hassio narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button> <ha-menu-button
hassio
narrow="[[narrow]]"
show-menu="[[showMenu]]"
></ha-menu-button>
<div main-title>Hass.io</div> <div main-title>Hass.io</div>
<template is="dom-if" if="[[showRefreshButton(page)]]"> <template is="dom-if" if="[[showRefreshButton(page)]]">
<paper-icon-button icon="hassio:refresh" on-click="refreshClicked"></paper-icon-button> <paper-icon-button
icon="hassio:refresh"
on-click="refreshClicked"
></paper-icon-button>
</template> </template>
</app-toolbar> </app-toolbar>
<paper-tabs scrollable="" selected="[[page]]" attr-for-selected="page-name" on-iron-activate="handlePageSelected"> <paper-tabs
scrollable=""
selected="[[page]]"
attr-for-selected="page-name"
on-iron-activate="handlePageSelected"
>
<paper-tab page-name="dashboard">Dashboard</paper-tab> <paper-tab page-name="dashboard">Dashboard</paper-tab>
<paper-tab page-name="snapshots">Snapshots</paper-tab> <paper-tab page-name="snapshots">Snapshots</paper-tab>
<paper-tab page-name="store">Add-on store</paper-tab> <paper-tab page-name="store">Add-on store</paper-tab>
@ -51,25 +63,45 @@ class HassioPagesWithTabs extends NavigateMixin(PolymerElement) {
</paper-tabs> </paper-tabs>
</app-header> </app-header>
<template is="dom-if" if="[[equals(page, &quot;dashboard&quot;)]]"> <template is="dom-if" if="[[equals(page, &quot;dashboard&quot;)]]">
<hassio-dashboard hass="[[hass]]" supervisor-info="[[supervisorInfo]]" hass-info="[[hassInfo]]"></hassio-dashboard> <hassio-dashboard
hass="[[hass]]"
supervisor-info="[[supervisorInfo]]"
hass-info="[[hassInfo]]"
></hassio-dashboard>
</template> </template>
<template is="dom-if" if="[[equals(page, &quot;snapshots&quot;)]]"> <template is="dom-if" if="[[equals(page, &quot;snapshots&quot;)]]">
<hassio-snapshots hass="[[hass]]" installed-addons="[[supervisorInfo.addons]]" snapshot-slug="{{snapshotSlug}}" snapshot-deleted="{{snapshotDeleted}}"></hassio-snapshots> <hassio-snapshots
hass="[[hass]]"
installed-addons="[[supervisorInfo.addons]]"
snapshot-slug="{{snapshotSlug}}"
snapshot-deleted="{{snapshotDeleted}}"
></hassio-snapshots>
</template> </template>
<template is="dom-if" if="[[equals(page, &quot;store&quot;)]]"> <template is="dom-if" if="[[equals(page, &quot;store&quot;)]]">
<hassio-addon-store hass="[[hass]]"></hassio-addon-store> <hassio-addon-store hass="[[hass]]"></hassio-addon-store>
</template> </template>
<template is="dom-if" if="[[equals(page, &quot;system&quot;)]]"> <template is="dom-if" if="[[equals(page, &quot;system&quot;)]]">
<hassio-system hass="[[hass]]" supervisor-info="[[supervisorInfo]]" host-info="[[hostInfo]]"></hassio-system> <hassio-system
hass="[[hass]]"
supervisor-info="[[supervisorInfo]]"
host-info="[[hostInfo]]"
></hassio-system>
</template> </template>
</app-header-layout> </app-header-layout>
<hassio-markdown-dialog title="[[markdownTitle]]" content="[[markdownContent]]"></hassio-markdown-dialog> <hassio-markdown-dialog
title="[[markdownTitle]]"
content="[[markdownContent]]"
></hassio-markdown-dialog>
<template is="dom-if" if="[[equals(page, &quot;snapshots&quot;)]]"> <template is="dom-if" if="[[equals(page, &quot;snapshots&quot;)]]">
<hassio-snapshot hass="[[hass]]" snapshot-slug="{{snapshotSlug}}" snapshot-deleted="{{snapshotDeleted}}"></hassio-snapshot> <hassio-snapshot
hass="[[hass]]"
snapshot-slug="{{snapshotSlug}}"
snapshot-deleted="{{snapshotDeleted}}"
></hassio-snapshot>
</template> </template>
`; `;
} }
static get properties() { static get properties() {

View File

@ -7,6 +7,7 @@ import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import { getSignedPath } from "../../../src/auth/data";
import "../../../src/resources/ha-style"; import "../../../src/resources/ha-style";
@ -56,13 +57,20 @@ class HassioSnapshot extends PolymerElement {
color: var(--google-red-500); color: var(--google-red-500);
} }
</style> </style>
<paper-dialog id="dialog" with-backdrop="" on-iron-overlay-closed="_dialogClosed"> <paper-dialog
id="dialog"
with-backdrop=""
on-iron-overlay-closed="_dialogClosed"
>
<app-toolbar> <app-toolbar>
<paper-icon-button icon="hassio:close" dialog-dismiss=""></paper-icon-button> <paper-icon-button
icon="hassio:close"
dialog-dismiss=""
></paper-icon-button>
<div main-title="">[[_computeName(snapshot)]]</div> <div main-title="">[[_computeName(snapshot)]]</div>
</app-toolbar> </app-toolbar>
<div class="details"> <div class="details">
[[_computeType(snapshot.type)]] ([[_computeSize(snapshot.size)]])<br> [[_computeType(snapshot.type)]] ([[_computeSize(snapshot.size)]])<br />
[[_formatDatetime(snapshot.date)]] [[_formatDatetime(snapshot.date)]]
</div> </div>
<div>Home Assistant:</div> <div>Home Assistant:</div>
@ -80,32 +88,52 @@ class HassioSnapshot extends PolymerElement {
<template is="dom-if" if="[[snapshot.addons.length]]"> <template is="dom-if" if="[[snapshot.addons.length]]">
<div>Add-ons:</div> <div>Add-ons:</div>
<paper-dialog-scrollable> <paper-dialog-scrollable>
<template is="dom-repeat" items="[[snapshot.addons]]" sort="_sortAddons"> <template
is="dom-repeat"
items="[[snapshot.addons]]"
sort="_sortAddons"
>
<paper-checkbox checked="{{item.checked}}"> <paper-checkbox checked="{{item.checked}}">
[[item.name]] [[item.name]] <span class="details">([[item.version]])</span>
<span class="details">([[item.version]])</span>
</paper-checkbox> </paper-checkbox>
</template> </template>
</paper-dialog-scrollable> </paper-dialog-scrollable>
</template> </template>
<template is="dom-if" if="[[snapshot.protected]]"> <template is="dom-if" if="[[snapshot.protected]]">
<paper-input autofocus="" label="Password" type="password" value="{{snapshotPassword}}"></paper-input> <paper-input
autofocus=""
label="Password"
type="password"
value="{{snapshotPassword}}"
></paper-input>
</template> </template>
<template is="dom-if" if="[[error]]"> <template is="dom-if" if="[[error]]">
<p class="error">Error: [[error]]</p> <p class="error">Error: [[error]]</p>
</template> </template>
<div class="buttons"> <div class="buttons">
<paper-icon-button icon="hassio:delete" on-click="_deleteClicked" class="warning" title="Delete snapshot"></paper-icon-button> <paper-icon-button
<a href="[[_computeDownloadUrl(snapshotSlug)]]" download="[[_computeDownloadName(snapshot)]]"> icon="hassio:delete"
<paper-icon-button icon="hassio:download" class="download" title="Download snapshot"></paper-icon-button> on-click="_deleteClicked"
</a> class="warning"
<paper-button on-click="_partialRestoreClicked">Restore selected</paper-button> title="Delete snapshot"
></paper-icon-button>
<paper-icon-button
on-click="_downloadClicked"
icon="hassio:download"
class="download"
title="Download snapshot"
></paper-icon-button>
<paper-button on-click="_partialRestoreClicked"
>Restore selected</paper-button
>
<template is="dom-if" if="[[_isFullSnapshot(snapshot.type)]]"> <template is="dom-if" if="[[_isFullSnapshot(snapshot.type)]]">
<paper-button on-click="_fullRestoreClicked">Wipe &amp; restore</paper-button> <paper-button on-click="_fullRestoreClicked"
>Wipe &amp; restore</paper-button
>
</template> </template>
</div> </div>
</paper-dialog> </paper-dialog>
`; `;
} }
static get properties() { static get properties() {
@ -251,14 +279,24 @@ class HassioSnapshot extends PolymerElement {
); );
} }
_computeDownloadUrl(snapshotSlug) { async _downloadClicked() {
const password = encodeURIComponent(this.hass.connection.options.authToken); let signedPath;
return `/api/hassio/snapshots/${snapshotSlug}/download?api_password=${password}`; try {
signedPath = await getSignedPath(
this.hass,
`/api/hassio/snapshots/${this.snapshotSlug}/download`
);
} catch (err) {
alert(`Error: ${err.message}`);
return;
} }
const name = this._computeName(this.snapshot).replace(/[^a-z0-9]+/gi, "_");
_computeDownloadName(snapshot) { const a = document.createElement("A");
const name = this._computeName(snapshot).replace(/[^a-z0-9]+/gi, "_"); a.href = signedPath.path;
return `Hass_io_${name}.tar`; a.download = `Hass_io_${name}.tar`;
this.$.dialog.appendChild(a);
a.click();
this.$.dialog.removeChild(a);
} }
_computeName(snapshot) { _computeName(snapshot) {

View File

@ -36,13 +36,17 @@ class HassioSnapshots extends EventsMixin(PolymerElement) {
<div class="title"> <div class="title">
Create snapshot Create snapshot
<div class="description"> <div class="description">
Snapshots allow you to easily backup and Snapshots allow you to easily backup and restore all data of your
restore all data of your Hass.io instance. Hass.io instance.
</div> </div>
</div> </div>
<paper-card> <paper-card>
<div class="card-content"> <div class="card-content">
<paper-input autofocus="" label="Name" value="{{snapshotName}}"></paper-input> <paper-input
autofocus=""
label="Name"
value="{{snapshotName}}"
></paper-input>
Type: Type:
<paper-radio-group selected="{{snapshotType}}"> <paper-radio-group selected="{{snapshotType}}">
<paper-radio-button name="full"> <paper-radio-button name="full">
@ -60,23 +64,37 @@ class HassioSnapshots extends EventsMixin(PolymerElement) {
</paper-checkbox> </paper-checkbox>
</template> </template>
Add-ons: Add-ons:
<template is="dom-repeat" items="[[addonList]]" sort="_sortAddons"> <template
is="dom-repeat"
items="[[addonList]]"
sort="_sortAddons"
>
<paper-checkbox checked="{{item.checked}}"> <paper-checkbox checked="{{item.checked}}">
[[item.name]] [[item.name]]
</paper-checkbox> </paper-checkbox>
</template> </template>
</template> </template>
Security: Security:
<paper-checkbox checked="{{snapshotHasPassword}}">Password protection</paper-checkbox> <paper-checkbox checked="{{snapshotHasPassword}}"
>Password protection</paper-checkbox
>
<template is="dom-if" if="[[snapshotHasPassword]]"> <template is="dom-if" if="[[snapshotHasPassword]]">
<paper-input label="Password" type="password" value="{{snapshotPassword}}"></paper-input> <paper-input
label="Password"
type="password"
value="{{snapshotPassword}}"
></paper-input>
</template> </template>
<template is="dom-if" if="[[error]]"> <template is="dom-if" if="[[error]]">
<p class="error">[[error]]</p> <p class="error">[[error]]</p>
</template> </template>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<paper-button disabled="[[creatingSnapshot]]" on-click="_createSnapshot">Create</paper-button> <paper-button
disabled="[[creatingSnapshot]]"
on-click="_createSnapshot"
>Create</paper-button
>
</div> </div>
</paper-card> </paper-card>
</div> </div>
@ -88,16 +106,28 @@ class HassioSnapshots extends EventsMixin(PolymerElement) {
<div class="card-content">You don't have any snapshots yet.</div> <div class="card-content">You don't have any snapshots yet.</div>
</paper-card> </paper-card>
</template> </template>
<template is="dom-repeat" items="[[snapshots]]" as="snapshot" sort="_sortSnapshots"> <template
is="dom-repeat"
items="[[snapshots]]"
as="snapshot"
sort="_sortSnapshots"
>
<paper-card class="pointer" on-click="_snapshotClicked"> <paper-card class="pointer" on-click="_snapshotClicked">
<div class="card-content"> <div class="card-content">
<hassio-card-content hass="[[hass]]" title="[[_computeName(snapshot)]]" description="[[_computeDetails(snapshot)]]" datetime="[[snapshot.date]]" icon="[[_computeIcon(snapshot.type)]]" icon-class="snapshot"></hassio-card-content> <hassio-card-content
hass="[[hass]]"
title="[[_computeName(snapshot)]]"
description="[[_computeDetails(snapshot)]]"
datetime="[[snapshot.date]]"
icon="[[_computeIcon(snapshot.type)]]"
icon-class="snapshot"
></hassio-card-content>
</div> </div>
</paper-card> </paper-card>
</template> </template>
</div> </div>
</div> </div>
`; `;
} }
static get properties() { static get properties() {

View File

@ -50,7 +50,8 @@ class HassioHostInfo extends EventsMixin(PolymerElement) {
<div class="card-content"> <div class="card-content">
<h2>Host system</h2> <h2>Host system</h2>
<table class="info"> <table class="info">
<tbody><tr> <tbody>
<tr>
<td>Hostname</td> <td>Hostname</td>
<td>[[data.hostname]]</td> <td>[[data.hostname]]</td>
</tr> </tr>
@ -64,7 +65,8 @@ class HassioHostInfo extends EventsMixin(PolymerElement) {
<td>[[data.deployment]]</td> <td>[[data.deployment]]</td>
</tr> </tr>
</template> </template>
</tbody></table> </tbody>
</table>
<paper-button raised on-click="_showHardware" class="info"> <paper-button raised on-click="_showHardware" class="info">
Hardware Hardware
</paper-button> </paper-button>
@ -79,20 +81,38 @@ class HassioHostInfo extends EventsMixin(PolymerElement) {
</div> </div>
<div class="card-actions"> <div class="card-actions">
<template is="dom-if" if="[[_featureAvailable(data, 'reboot')]]"> <template is="dom-if" if="[[_featureAvailable(data, 'reboot')]]">
<ha-call-api-button class="warning" hass="[[hass]]" path="hassio/host/reboot">Reboot</ha-call-api-button> <ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/host/reboot"
>Reboot</ha-call-api-button
>
</template> </template>
<template is="dom-if" if="[[_featureAvailable(data, 'shutdown')]]"> <template is="dom-if" if="[[_featureAvailable(data, 'shutdown')]]">
<ha-call-api-button class="warning" hass="[[hass]]" path="hassio/host/shutdown">Shutdown</ha-call-api-button> <ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/host/shutdown"
>Shutdown</ha-call-api-button
>
</template> </template>
<template is="dom-if" if="[[_featureAvailable(data, 'hassos')]]"> <template is="dom-if" if="[[_featureAvailable(data, 'hassos')]]">
<ha-call-api-button class="warning" hass="[[hass]]" path="hassio/hassos/config/sync" title="Load HassOS configs or updates from USB">Import from USB</ha-call-api-button> <ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/hassos/config/sync"
title="Load HassOS configs or updates from USB"
>Import from USB</ha-call-api-button
>
</template> </template>
<template is="dom-if" if="[[_computeUpdateAvailable(_hassOs)]]"> <template is="dom-if" if="[[_computeUpdateAvailable(_hassOs)]]">
<ha-call-api-button hass="[[hass]]" path="hassio/hassos/update">Update</ha-call-api-button> <ha-call-api-button hass="[[hass]]" path="hassio/hassos/update"
>Update</ha-call-api-button
>
</template> </template>
</div> </div>
</paper-card> </paper-card>
`; `;
} }
static get properties() { static get properties() {

View File

@ -41,41 +41,64 @@ class HassioSupervisorInfo extends EventsMixin(PolymerElement) {
<div class="card-content"> <div class="card-content">
<h2>Hass.io supervisor</h2> <h2>Hass.io supervisor</h2>
<table class="info"> <table class="info">
<tbody><tr> <tbody>
<tr>
<td>Version</td> <td>Version</td>
<td> <td>[[data.version]]</td>
[[data.version]]
</td>
</tr> </tr>
<tr> <tr>
<td>Latest version</td> <td>Latest version</td>
<td>[[data.last_version]]</td> <td>[[data.last_version]]</td>
</tr> </tr>
<template is="dom-if" if="[[!_equals(data.channel, &quot;stable&quot;)]]"> <template
is="dom-if"
if="[[!_equals(data.channel, &quot;stable&quot;)]]"
>
<tr> <tr>
<td>Channel</td> <td>Channel</td>
<td>[[data.channel]]</td> <td>[[data.channel]]</td>
</tr> </tr>
</template> </template>
</tbody></table> </tbody>
</table>
<template is="dom-if" if="[[errors]]"> <template is="dom-if" if="[[errors]]">
<div class="errors">Error: [[errors]]</div> <div class="errors">Error: [[errors]]</div>
</template> </template>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-call-api-button hass="[[hass]]" path="hassio/supervisor/reload">Reload</ha-call-api-button> <ha-call-api-button hass="[[hass]]" path="hassio/supervisor/reload"
>Reload</ha-call-api-button
>
<template is="dom-if" if="[[computeUpdateAvailable(data)]]"> <template is="dom-if" if="[[computeUpdateAvailable(data)]]">
<ha-call-api-button hass="[[hass]]" path="hassio/supervisor/update">Update</ha-call-api-button> <ha-call-api-button hass="[[hass]]" path="hassio/supervisor/update"
>Update</ha-call-api-button
>
</template> </template>
<template is="dom-if" if="[[_equals(data.channel, &quot;beta&quot;)]]"> <template
<ha-call-api-button hass="[[hass]]" path="hassio/supervisor/options" data="[[leaveBeta]]">Leave beta channel</ha-call-api-button> is="dom-if"
if="[[_equals(data.channel, &quot;beta&quot;)]]"
>
<ha-call-api-button
hass="[[hass]]"
path="hassio/supervisor/options"
data="[[leaveBeta]]"
>Leave beta channel</ha-call-api-button
>
</template> </template>
<template is="dom-if" if="[[_equals(data.channel, &quot;stable&quot;)]]"> <template
<paper-button on-click="_joinBeta" class="warning" title="Get beta updates for Home Assistant (RCs), supervisor and host">Join beta channel</paper-button> is="dom-if"
if="[[_equals(data.channel, &quot;stable&quot;)]]"
>
<paper-button
on-click="_joinBeta"
class="warning"
title="Get beta updates for Home Assistant (RCs), supervisor and host"
>Join beta channel</paper-button
>
</template> </template>
</div> </div>
</paper-card> </paper-card>
`; `;
} }
static get properties() { static get properties() {

View File

@ -15,14 +15,12 @@ class HassioSupervisorLog extends PolymerElement {
} }
</style> </style>
<paper-card> <paper-card>
<div class="card-content"> <div class="card-content"><pre>[[log]]</pre></div>
<pre>[[log]]</pre>
</div>
<div class="card-actions"> <div class="card-actions">
<paper-button on-click="refreshTapped">Refresh</paper-button> <paper-button on-click="refreshTapped">Refresh</paper-button>
</div> </div>
</paper-card> </paper-card>
`; `;
} }
static get properties() { static get properties() {

View File

@ -23,12 +23,18 @@ class HassioSystem extends PolymerElement {
</style> </style>
<div class="content"> <div class="content">
<div class="title">Information</div> <div class="title">Information</div>
<hassio-supervisor-info hass="[[hass]]" data="[[supervisorInfo]]"></hassio-supervisor-info> <hassio-supervisor-info
<hassio-host-info hass="[[hass]]" data="[[hostInfo]]"></hassio-host-info> hass="[[hass]]"
data="[[supervisorInfo]]"
></hassio-supervisor-info>
<hassio-host-info
hass="[[hass]]"
data="[[hostInfo]]"
></hassio-host-info>
<div class="title">System log</div> <div class="title">System log</div>
<hassio-supervisor-log hass="[[hass]]"></hassio-supervisor-log> <hassio-supervisor-log hass="[[hass]]"></hassio-supervisor-log>
</div> </div>
`; `;
} }
static get properties() { static get properties() {

View File

@ -38,7 +38,7 @@ module.exports = {
}, },
}), }),
isProdBuild && isProdBuild &&
isCI && !isCI &&
new CompressionPlugin({ new CompressionPlugin({
cache: true, cache: true,
exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/], exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/],

View File

@ -8,8 +8,8 @@
"version": "1.0.0", "version": "1.0.0",
"scripts": { "scripts": {
"build": "script/build_frontend", "build": "script/build_frontend",
"lint": "eslint src hassio/src gallery/src test-mocha && tslint -c tslint.json 'src/**/*.ts' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'test-mocha/**/*.ts' && polymer lint && tsc", "lint": "eslint src hassio/src gallery/src && tslint 'src/**/*.ts' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'test-mocha/**/*.ts' && polymer lint && tsc",
"mocha": "node_modules/.bin/mocha --opts test-mocha/mocha.opts", "mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --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", "docker_build": "sh ./script/docker_run.sh build $npm_package_version",
"bash": "sh ./script/docker_run.sh bash $npm_package_version" "bash": "sh ./script/docker_run.sh bash $npm_package_version"
@ -35,7 +35,7 @@
"@polymer/iron-media-query": "^3.0.1", "@polymer/iron-media-query": "^3.0.1",
"@polymer/iron-pages": "^3.0.1", "@polymer/iron-pages": "^3.0.1",
"@polymer/iron-resizable-behavior": "^3.0.1", "@polymer/iron-resizable-behavior": "^3.0.1",
"@polymer/lit-element": "^0.6.2", "@polymer/lit-element": "0.6.2",
"@polymer/neon-animation": "^3.0.1", "@polymer/neon-animation": "^3.0.1",
"@polymer/paper-button": "^3.0.1", "@polymer/paper-button": "^3.0.1",
"@polymer/paper-card": "^3.0.1", "@polymer/paper-card": "^3.0.1",
@ -73,12 +73,12 @@
"es6-object-assign": "^1.1.0", "es6-object-assign": "^1.1.0",
"eslint-import-resolver-webpack": "^0.10.1", "eslint-import-resolver-webpack": "^0.10.1",
"fecha": "^2.3.3", "fecha": "^2.3.3",
"home-assistant-js-websocket": "^3.1.5", "home-assistant-js-websocket": "^3.2.4",
"intl-messageformat": "^2.2.0", "intl-messageformat": "^2.2.0",
"jquery": "^3.3.1", "jquery": "^3.3.1",
"js-yaml": "^3.12.0", "js-yaml": "^3.12.0",
"leaflet": "^1.3.4", "leaflet": "^1.3.4",
"lit-html": "^0.12.0", "lit-html": "0.12.0",
"marked": "^0.5.0", "marked": "^0.5.0",
"mdn-polyfills": "^5.12.0", "mdn-polyfills": "^5.12.0",
"moment": "^2.22.2", "moment": "^2.22.2",
@ -100,10 +100,12 @@
"@babel/preset-env": "^7.1.0", "@babel/preset-env": "^7.1.0",
"@babel/preset-typescript": "^7.1.0", "@babel/preset-typescript": "^7.1.0",
"@gfx/zopfli": "^1.0.9", "@gfx/zopfli": "^1.0.9",
"@types/chai": "^4.1.7",
"@types/mocha": "^5.2.5",
"babel-eslint": "^10", "babel-eslint": "^10",
"babel-loader": "^8.0.4", "babel-loader": "^8.0.4",
"babel-minify-webpack-plugin": "^0.3.1", "babel-minify-webpack-plugin": "^0.3.1",
"chai": "^4.1.2", "chai": "^4.2.0",
"compression-webpack-plugin": "^2.0.0", "compression-webpack-plugin": "^2.0.0",
"copy-webpack-plugin": "^4.5.2", "copy-webpack-plugin": "^4.5.2",
"del": "^3.0.0", "del": "^3.0.0",
@ -135,7 +137,8 @@
"raw-loader": "^0.5.1", "raw-loader": "^0.5.1",
"reify": "^0.18.1", "reify": "^0.18.1",
"require-dir": "^1.0.0", "require-dir": "^1.0.0",
"sinon": "^7.1.0", "sinon": "^7.1.1",
"ts-mocha": "^2.0.0",
"tslint": "^5.11.0", "tslint": "^5.11.0",
"tslint-config-prettier": "^1.15.0", "tslint-config-prettier": "^1.15.0",
"tslint-eslint-rules": "^5.4.0", "tslint-eslint-rules": "^5.4.0",
@ -158,7 +161,9 @@
"@webcomponents/shadycss": "^1.5.2", "@webcomponents/shadycss": "^1.5.2",
"@vaadin/vaadin-overlay": "3.2.0-alpha3", "@vaadin/vaadin-overlay": "3.2.0-alpha3",
"@vaadin/vaadin-lumo-styles": "1.2.0", "@vaadin/vaadin-lumo-styles": "1.2.0",
"fecha": "https://github.com/balloob/fecha/archive/51d14fd0eb4781e2ecf265d1c3080706259133b5.tar.gz" "fecha": "https://github.com/taylorhakes/fecha/archive/5e8fe08d982647fdb19fb403459838b02647813c.tar.gz",
"lit-html": "0.12.0",
"@polymer/lit-element": "0.6.2"
}, },
"main": "src/home-assistant.js", "main": "src/home-assistant.js",
"husky": { "husky": {

View File

@ -8,9 +8,11 @@ function patch(version) {
return `${parts[0]}.${Number(parts[1]) + 1}`; return `${parts[0]}.${Number(parts[1]) + 1}`;
} }
function today(version) { function today() {
const now = new Date(); const now = new Date();
return `${now.getFullYear()}${now.getMonth() + 1}${now.getDate()}.0`; return `${now.getFullYear()}${now.getMonth() + 1}${String(
now.getDate()
).padStart(2, "0")}.0`;
} }
const methods = { const methods = {

View File

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup( setup(
name="home-assistant-frontend", name="home-assistant-frontend",
version="20181103.3", version="20181121.0",
description="The Home Assistant frontend", description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer", url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors", author="The Home Assistant Authors",

7
src/auth/data.ts Normal file
View File

@ -0,0 +1,7 @@
import { HomeAssistant } from "../types";
import { SignedPath } from "./types";
export const getSignedPath = (
hass: HomeAssistant,
path: string
): Promise<SignedPath> => hass.callWS({ type: "auth/sign_path", path });

View File

@ -25,17 +25,25 @@ class HaAuthFlow extends LocalizeLiteMixin(PolymerElement) {
[[localize('ui.panel.page-authorize.form.working')]]: [[localize('ui.panel.page-authorize.form.working')]]:
</template> </template>
<template is="dom-if" if="[[_equals(_state, &quot;error&quot;)]]"> <template is="dom-if" if="[[_equals(_state, &quot;error&quot;)]]">
<div class='error'>Error: [[_errorMsg]]</div> <div class="error">Error: [[_errorMsg]]</div>
</template> </template>
<template is="dom-if" if="[[_equals(_state, &quot;step&quot;)]]"> <template is="dom-if" if="[[_equals(_state, &quot;step&quot;)]]">
<template is="dom-if" if="[[_equals(_step.type, &quot;abort&quot;)]]"> <template is="dom-if" if="[[_equals(_step.type, &quot;abort&quot;)]]">
[[localize('ui.panel.page-authorize.abort_intro')]]: [[localize('ui.panel.page-authorize.abort_intro')]]:
<ha-markdown content="[[_computeStepAbortedReason(localize, _step)]]"></ha-markdown> <ha-markdown
content="[[_computeStepAbortedReason(localize, _step)]]"
></ha-markdown>
</template> </template>
<template is="dom-if" if="[[_equals(_step.type, &quot;form&quot;)]]"> <template is="dom-if" if="[[_equals(_step.type, &quot;form&quot;)]]">
<template is="dom-if" if="[[_computeStepDescription(localize, _step)]]"> <template
<ha-markdown content="[[_computeStepDescription(localize, _step)]]" allow-svg></ha-markdown> is="dom-if"
if="[[_computeStepDescription(localize, _step)]]"
>
<ha-markdown
content="[[_computeStepDescription(localize, _step)]]"
allow-svg
></ha-markdown>
</template> </template>
<ha-form <ha-form
@ -46,15 +54,14 @@ class HaAuthFlow extends LocalizeLiteMixin(PolymerElement) {
compute-error="[[_computeErrorCallback(localize, _step)]]" compute-error="[[_computeErrorCallback(localize, _step)]]"
></ha-form> ></ha-form>
</template> </template>
<div class='action'> <div class="action">
<paper-button <paper-button raised on-click="_handleSubmit"
raised >[[_computeSubmitCaption(_step.type)]]</paper-button
on-click='_handleSubmit' >
>[[_computeSubmitCaption(_step.type)]]</paper-button>
</div> </div>
</template> </template>
</form> </form>
`; `;
} }
static get properties() { static get properties() {

View File

@ -20,7 +20,7 @@ class HaAuthorize extends LocalizeLiteMixin(PolymerElement) {
ha-markdown a { ha-markdown a {
color: var(--primary-color); color: var(--primary-color);
} }
ha-markdown p:last-child{ ha-markdown p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
ha-pick-auth-provider { ha-pick-auth-provider {
@ -34,7 +34,9 @@ class HaAuthorize extends LocalizeLiteMixin(PolymerElement) {
</template> </template>
<template is="dom-if" if="[[_authProviders]]"> <template is="dom-if" if="[[_authProviders]]">
<ha-markdown content='[[_computeIntro(localize, clientId, _authProvider)]]'></ha-markdown> <ha-markdown
content="[[_computeIntro(localize, clientId, _authProvider)]]"
></ha-markdown>
<ha-auth-flow <ha-auth-flow
resources="[[resources]]" resources="[[resources]]"
@ -54,7 +56,7 @@ class HaAuthorize extends LocalizeLiteMixin(PolymerElement) {
></ha-pick-auth-provider> ></ha-pick-auth-provider>
</template> </template>
</template> </template>
`; `;
} }
static get properties() { static get properties() {

View File

@ -29,7 +29,7 @@ class HaPickAuthProvider extends EventsMixin(
<iron-icon icon="hass:chevron-right"></iron-icon> <iron-icon icon="hass:chevron-right"></iron-icon>
</paper-item> </paper-item>
</template> </template>
`; `;
} }
static get properties() { static get properties() {

3
src/auth/types.ts Normal file
View File

@ -0,0 +1,3 @@
export interface SignedPath {
path: string;
}

View File

@ -13,9 +13,12 @@ class HaBadgesCard extends PolymerElement {
} }
</style> </style>
<template is="dom-repeat" items="[[states]]"> <template is="dom-repeat" items="[[states]]">
<ha-state-label-badge hass="[[hass]]" state="[[item]]"></ha-state-label-badge> <ha-state-label-badge
hass="[[hass]]"
state="[[item]]"
></ha-state-label-badge>
</template> </template>
`; `;
} }
static get properties() { static get properties() {

View File

@ -50,7 +50,11 @@ class HaCameraCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
</style> </style>
<template is="dom-if" if="[[cameraFeedSrc]]"> <template is="dom-if" if="[[cameraFeedSrc]]">
<img src="[[cameraFeedSrc]]" class="camera-feed" alt="[[_computeStateName(stateObj)]]"> <img
src="[[cameraFeedSrc]]"
class="camera-feed"
alt="[[_computeStateName(stateObj)]]"
/>
</template> </template>
<div class="caption"> <div class="caption">
[[_computeStateName(stateObj)]] [[_computeStateName(stateObj)]]
@ -58,7 +62,7 @@ class HaCameraCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
([[localize('ui.card.camera.not_available')]]) ([[localize('ui.card.camera.not_available')]])
</template> </template>
</div> </div>
`; `;
} }
static get properties() { static get properties() {

View File

@ -48,22 +48,36 @@ class HaEntitiesCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
<ha-card> <ha-card>
<template is="dom-if" if="[[title]]"> <template is="dom-if" if="[[title]]">
<div class$="[[computeTitleClass(groupEntity)]]" on-click="entityTapped"> <div
class$="[[computeTitleClass(groupEntity)]]"
on-click="entityTapped"
>
<div class="flex name">[[title]]</div> <div class="flex name">[[title]]</div>
<template is="dom-if" if="[[showGroupToggle(groupEntity, states)]]"> <template is="dom-if" if="[[showGroupToggle(groupEntity, states)]]">
<ha-entity-toggle hass="[[hass]]" state-obj="[[groupEntity]]"></ha-entity-toggle> <ha-entity-toggle
hass="[[hass]]"
state-obj="[[groupEntity]]"
></ha-entity-toggle>
</template> </template>
</div> </div>
</template> </template>
<div class="states"> <div class="states">
<template is="dom-repeat" items="[[states]]" on-dom-change="addTapEvents"> <template
is="dom-repeat"
items="[[states]]"
on-dom-change="addTapEvents"
>
<div class$="[[computeStateClass(item)]]"> <div class$="[[computeStateClass(item)]]">
<state-card-content hass="[[hass]]" class="state-card" state-obj="[[item]]"></state-card-content> <state-card-content
hass="[[hass]]"
class="state-card"
state-obj="[[item]]"
></state-card-content>
</div> </div>
</template> </template>
</div> </div>
</ha-card> </ha-card>
`; `;
} }
static get properties() { static get properties() {

View File

@ -38,15 +38,32 @@ class HaHistoryGraphCard extends EventsMixin(PolymerElement) {
display: none; display: none;
} }
</style> </style>
<ha-state-history-data hass="[[hass]]" filter-type="recent-entity" entity-id="[[computeHistoryEntities(stateObj)]]" data="{{stateHistory}}" is-loading="{{stateHistoryLoading}}" cache-config="[[cacheConfig]]"></ha-state-history-data> <ha-state-history-data
<paper-card dialog$="[[inDialog]]" on-click="cardTapped" elevation="[[computeElevation(inDialog)]]"> hass="[[hass]]"
filter-type="recent-entity"
entity-id="[[computeHistoryEntities(stateObj)]]"
data="{{stateHistory}}"
is-loading="{{stateHistoryLoading}}"
cache-config="[[cacheConfig]]"
></ha-state-history-data>
<paper-card
dialog$="[[inDialog]]"
on-click="cardTapped"
elevation="[[computeElevation(inDialog)]]"
>
<div class="header">[[computeTitle(stateObj)]]</div> <div class="header">[[computeTitle(stateObj)]]</div>
<div class="content"> <div class="content">
<state-history-charts hass="[[hass]]" history-data="[[stateHistory]]" is-loading-data="[[stateHistoryLoading]]" up-to-now no-single> <state-history-charts
hass="[[hass]]"
history-data="[[stateHistory]]"
is-loading-data="[[stateHistoryLoading]]"
up-to-now
no-single
>
</state-history-charts> </state-history-charts>
</div> </div>
</paper-card> </paper-card>
`; `;
} }
static get properties() { static get properties() {

View File

@ -18,7 +18,9 @@ import LocalizeMixin from "../mixins/localize-mixin";
class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) { class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() { static get template() {
return html` return html`
<style include="paper-material-styles iron-flex iron-flex-alignment iron-positioning"> <style
include="paper-material-styles iron-flex iron-flex-alignment iron-positioning"
>
:host { :host {
@apply --paper-material-elevation-1; @apply --paper-material-elevation-1;
display: block; display: block;
@ -40,7 +42,7 @@ class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
width: 100%; width: 100%;
/* removed .25% from 16:9 ratio to fix YT black bars */ /* removed .25% from 16:9 ratio to fix YT black bars */
padding-top: 56%; padding-top: 56%;
transition: padding-top .8s; transition: padding-top 0.8s;
} }
.banner.no-cover { .banner.no-cover {
@ -70,7 +72,7 @@ class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
background-position: center center; background-position: center center;
background-size: cover; background-size: cover;
transition: opacity .8s; transition: opacity 0.8s;
opacity: 1; opacity: 1;
} }
@ -94,7 +96,7 @@ class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
font-weight: 500; font-weight: 500;
color: white; color: white;
transition: background-color .5s; transition: background-color 0.5s;
} }
.banner.is-off > .caption { .banner.is-off > .caption {
@ -110,9 +112,9 @@ class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
.progress { .progress {
width: 100%; width: 100%;
height: var(--paper-progress-height, 4px); height: var(--paper-progress-height, 4px);
margin-top: calc(-1*var(--paper-progress-height, 4px)); margin-top: calc(-1 * var(--paper-progress-height, 4px));
--paper-progress-active-color: var(--accent-color); --paper-progress-active-color: var(--accent-color);
--paper-progress-container-color: rgba(200,200,200,0.5); --paper-progress-container-color: rgba(200, 200, 200, 0.5);
} }
.controls { .controls {
@ -144,7 +146,7 @@ class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
color: white; color: white;
border-radius: 50%; border-radius: 50%;
padding: 8px; padding: 8px;
transition: background-color .5s; transition: background-color 0.5s;
} }
paper-icon-button.primary[disabled] { paper-icon-button.primary[disabled] {
@ -162,25 +164,54 @@ class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
<div class="caption"> <div class="caption">
[[_computeStateName(stateObj)]] [[_computeStateName(stateObj)]]
<div class="title">[[computePrimaryText(localize, playerObj)]]</div> <div class="title">[[computePrimaryText(localize, playerObj)]]</div>
[[playerObj.secondaryTitle]]<br> [[playerObj.secondaryTitle]]<br />
</div> </div>
</div> </div>
<paper-progress max="[[stateObj.attributes.media_duration]]" value="[[playbackPosition]]" hidden$="[[computeHideProgress(playerObj)]]" class="progress"></paper-progress> <paper-progress
max="[[stateObj.attributes.media_duration]]"
value="[[playbackPosition]]"
hidden$="[[computeHideProgress(playerObj)]]"
class="progress"
></paper-progress>
<div class="controls layout horizontal justified"> <div class="controls layout horizontal justified">
<paper-icon-button icon="hass:power" on-click="handleTogglePower" invisible$="[[computeHidePowerButton(playerObj)]]" class="self-center secondary"></paper-icon-button> <paper-icon-button
icon="hass:power"
on-click="handleTogglePower"
invisible$="[[computeHidePowerButton(playerObj)]]"
class="self-center secondary"
></paper-icon-button>
<div> <div>
<paper-icon-button icon="hass:skip-previous" invisible$="[[!playerObj.supportsPreviousTrack]]" disabled="[[playerObj.isOff]]" on-click="handlePrevious"></paper-icon-button> <paper-icon-button
<paper-icon-button class="primary" icon="[[computePlaybackControlIcon(playerObj)]]" invisible$="[[!computePlaybackControlIcon(playerObj)]]" disabled="[[playerObj.isOff]]" on-click="handlePlaybackControl"></paper-icon-button> icon="hass:skip-previous"
<paper-icon-button icon="hass:skip-next" invisible$="[[!playerObj.supportsNextTrack]]" disabled="[[playerObj.isOff]]" on-click="handleNext"></paper-icon-button> invisible$="[[!playerObj.supportsPreviousTrack]]"
disabled="[[playerObj.isOff]]"
on-click="handlePrevious"
></paper-icon-button>
<paper-icon-button
class="primary"
icon="[[computePlaybackControlIcon(playerObj)]]"
invisible$="[[!computePlaybackControlIcon(playerObj)]]"
disabled="[[playerObj.isOff]]"
on-click="handlePlaybackControl"
></paper-icon-button>
<paper-icon-button
icon="hass:skip-next"
invisible$="[[!playerObj.supportsNextTrack]]"
disabled="[[playerObj.isOff]]"
on-click="handleNext"
></paper-icon-button>
</div> </div>
<paper-icon-button icon="hass:dots-vertical" on-click="handleOpenMoreInfo" class="self-center secondary"></paper-icon-button> <paper-icon-button
icon="hass:dots-vertical"
on-click="handleOpenMoreInfo"
class="self-center secondary"
></paper-icon-button>
</div> </div>
`; `;
} }
static get properties() { static get properties() {

View File

@ -46,9 +46,11 @@ class HaPersistentNotificationCard extends LocalizeMixin(PolymerElement) {
<ha-card header="[[computeTitle(stateObj)]]"> <ha-card header="[[computeTitle(stateObj)]]">
<ha-markdown content="[[stateObj.attributes.message]]"></ha-markdown> <ha-markdown content="[[stateObj.attributes.message]]"></ha-markdown>
<paper-button on-click="dismissTap">[[localize('ui.card.persistent_notification.dismiss')]]</paper-button> <paper-button on-click="dismissTap"
>[[localize('ui.card.persistent_notification.dismiss')]]</paper-button
>
</ha-card> </ha-card>
`; `;
} }
static get properties() { static get properties() {

View File

@ -34,7 +34,7 @@ class HaPlantCard extends EventsMixin(PolymerElement) {
padding: 16px; padding: 16px;
color: white; color: white;
width: 100%; width: 100%;
background:rgba(0, 0, 0, var(--dark-secondary-opacity)); background: rgba(0, 0, 0, var(--dark-secondary-opacity));
} }
.content { .content {
display: flex; display: flex;
@ -63,23 +63,40 @@ class HaPlantCard extends EventsMixin(PolymerElement) {
} }
</style> </style>
<ha-card class$="[[computeImageClass(stateObj.attributes.entity_picture)]]"> <ha-card
<div class="banner" style="background-image:url([[stateObj.attributes.entity_picture]])"> class$="[[computeImageClass(stateObj.attributes.entity_picture)]]"
>
<div
class="banner"
style="background-image:url([[stateObj.attributes.entity_picture]])"
>
<div class="header">[[computeTitle(stateObj)]]</div> <div class="header">[[computeTitle(stateObj)]]</div>
</div> </div>
<div class="content"> <div class="content">
<template is="dom-repeat" items="[[computeAttributes(stateObj.attributes)]]"> <template
is="dom-repeat"
items="[[computeAttributes(stateObj.attributes)]]"
>
<div class="attributes" on-click="attributeClicked"> <div class="attributes" on-click="attributeClicked">
<div><ha-icon icon="[[computeIcon(item, stateObj.attributes.battery)]]"></ha-icon></div> <div>
<div class$="[[computeAttributeClass(stateObj.attributes.problem, item)]]"> <ha-icon
icon="[[computeIcon(item, stateObj.attributes.battery)]]"
></ha-icon>
</div>
<div
class$="[[computeAttributeClass(stateObj.attributes.problem, item)]]"
>
[[computeValue(stateObj.attributes, item)]] [[computeValue(stateObj.attributes, item)]]
</div> </div>
<div class="uom">[[computeUom(stateObj.attributes.unit_of_measurement_dict, item)]]</div> <div class="uom">
[[computeUom(stateObj.attributes.unit_of_measurement_dict,
item)]]
</div>
</div> </div>
</template> </template>
</div> </div>
</ha-card> </ha-card>
`; `;
} }
static get properties() { static get properties() {

View File

@ -106,9 +106,7 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
<ha-card> <ha-card>
<div class="header"> <div class="header">
[[computeState(stateObj.state, localize)]] [[computeState(stateObj.state, localize)]]
<div class="name"> <div class="name">[[stateObj.attributes.friendly_name]]</div>
[[stateObj.attributes.friendly_name]]
</div>
</div> </div>
<div class="content"> <div class="content">
<div class="now"> <div class="now">
@ -117,26 +115,38 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
<ha-icon icon="[[getWeatherIcon(stateObj.state)]]"></ha-icon> <ha-icon icon="[[getWeatherIcon(stateObj.state)]]"></ha-icon>
</template> </template>
<div class="temp"> <div class="temp">
[[stateObj.attributes.temperature]]<span>[[getUnit('temperature')]]</span> [[stateObj.attributes.temperature]]<span
>[[getUnit('temperature')]]</span
>
</div> </div>
</div> </div>
<div class="attributes"> <div class="attributes">
<template is="dom-if" if="[[_showValue(stateObj.attributes.pressure)]]"> <template
is="dom-if"
if="[[_showValue(stateObj.attributes.pressure)]]"
>
<div> <div>
[[localize('ui.card.weather.attributes.air_pressure')]]: [[localize('ui.card.weather.attributes.air_pressure')]]:
[[stateObj.attributes.pressure]] [[getUnit('air_pressure')]] [[stateObj.attributes.pressure]] [[getUnit('air_pressure')]]
</div> </div>
</template> </template>
<template is="dom-if" if="[[_showValue(stateObj.attributes.humidity)]]"> <template
is="dom-if"
if="[[_showValue(stateObj.attributes.humidity)]]"
>
<div> <div>
[[localize('ui.card.weather.attributes.humidity')]]: [[localize('ui.card.weather.attributes.humidity')]]:
[[stateObj.attributes.humidity]] % [[stateObj.attributes.humidity]] %
</div> </div>
</template> </template>
<template is="dom-if" if="[[_showValue(stateObj.attributes.wind_speed)]]"> <template
is="dom-if"
if="[[_showValue(stateObj.attributes.wind_speed)]]"
>
<div> <div>
[[localize('ui.card.weather.attributes.wind_speed')]]: [[localize('ui.card.weather.attributes.wind_speed')]]:
[[getWind(stateObj.attributes.wind_speed, stateObj.attributes.wind_bearing, localize)]] [[getWind(stateObj.attributes.wind_speed,
stateObj.attributes.wind_bearing, localize)]]
</div> </div>
</template> </template>
</div> </div>
@ -145,22 +155,31 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
<div class="forecast"> <div class="forecast">
<template is="dom-repeat" items="[[forecast]]"> <template is="dom-repeat" items="[[forecast]]">
<div> <div>
<div class="weekday">[[computeDate(item.datetime)]]<br> <div class="weekday">
[[computeDate(item.datetime)]]<br />
<template is="dom-if" if="[[!_showValue(item.templow)]]"> <template is="dom-if" if="[[!_showValue(item.templow)]]">
[[computeTime(item.datetime)]] [[computeTime(item.datetime)]]
</template> </template>
</div> </div>
<template is="dom-if" if="[[_showValue(item.condition)]]"> <template is="dom-if" if="[[_showValue(item.condition)]]">
<div class="icon"> <div class="icon">
<ha-icon icon="[[getWeatherIcon(item.condition)]]"></ha-icon> <ha-icon
icon="[[getWeatherIcon(item.condition)]]"
></ha-icon>
</div> </div>
</template> </template>
<div class="temp">[[item.temperature]] [[getUnit('temperature')]]</div> <div class="temp">
[[item.temperature]] [[getUnit('temperature')]]
</div>
<template is="dom-if" if="[[_showValue(item.templow)]]"> <template is="dom-if" if="[[_showValue(item.templow)]]">
<div class="templow">[[item.templow]] [[getUnit('temperature')]]</div> <div class="templow">
[[item.templow]] [[getUnit('temperature')]]
</div>
</template> </template>
<template is="dom-if" if="[[_showValue(item.precipitation)]]"> <template is="dom-if" if="[[_showValue(item.precipitation)]]">
<div class="precipitation">[[item.precipitation]] [[getUnit('precipitation')]]</div> <div class="precipitation">
[[item.precipitation]] [[getUnit('precipitation')]]
</div>
</template> </template>
</div> </div>
</template> </template>

View File

@ -6,6 +6,34 @@ import { Auth } from "home-assistant-js-websocket";
const CALLBACK_SET_TOKEN = "externalAuthSetToken"; const CALLBACK_SET_TOKEN = "externalAuthSetToken";
const CALLBACK_REVOKE_TOKEN = "externalAuthRevokeToken"; const CALLBACK_REVOKE_TOKEN = "externalAuthRevokeToken";
interface BasePayload {
callback: string;
}
interface RefreshTokenResponse {
access_token: string;
expires_in: number;
}
declare global {
interface Window {
externalApp?: {
getExternalAuth(payload: BasePayload);
revokeExternalAuth(payload: BasePayload);
};
webkit?: {
messageHandlers: {
getExternalAuth: {
postMessage(payload: BasePayload);
};
revokeExternalAuth: {
postMessage(payload: BasePayload);
};
};
};
}
}
if (!window.externalApp && !window.webkit) { if (!window.externalApp && !window.webkit) {
throw new Error( throw new Error(
"External auth requires either externalApp or webkit defined on Window object." "External auth requires either externalApp or webkit defined on Window object."
@ -14,21 +42,24 @@ if (!window.externalApp && !window.webkit) {
export default class ExternalAuth extends Auth { export default class ExternalAuth extends Auth {
constructor(hassUrl) { constructor(hassUrl) {
super(); super({
this.data = {
hassUrl, hassUrl,
clientId: "",
refresh_token: "",
access_token: "", access_token: "",
expires_in: 0,
// This will trigger connection to do a refresh right away // This will trigger connection to do a refresh right away
expires: 0, expires: 0,
}; });
} }
async refreshAccessToken() { public async refreshAccessToken() {
const responseProm = new Promise((resolve, reject) => { const responseProm = new Promise<RefreshTokenResponse>(
(resolve, reject) => {
window[CALLBACK_SET_TOKEN] = (success, data) => window[CALLBACK_SET_TOKEN] = (success, data) =>
success ? resolve(data) : reject(data); success ? resolve(data) : reject(data);
}); }
);
// Allow promise to set resolve on window object. // Allow promise to set resolve on window object.
await 0; await 0;
@ -38,23 +69,18 @@ export default class ExternalAuth extends Auth {
if (window.externalApp) { if (window.externalApp) {
window.externalApp.getExternalAuth(callbackPayload); window.externalApp.getExternalAuth(callbackPayload);
} else { } else {
window.webkit.messageHandlers.getExternalAuth.postMessage( window.webkit!.messageHandlers.getExternalAuth.postMessage(
callbackPayload callbackPayload
); );
} }
// Response we expect back:
// {
// "access_token": "qwere",
// "expires_in": 1800
// }
const tokens = await responseProm; const tokens = await responseProm;
this.data.access_token = tokens.access_token; this.data.access_token = tokens.access_token;
this.data.expires = tokens.expires_in * 1000 + Date.now(); this.data.expires = tokens.expires_in * 1000 + Date.now();
} }
async revoke() { public async revoke() {
const responseProm = new Promise((resolve, reject) => { const responseProm = new Promise((resolve, reject) => {
window[CALLBACK_REVOKE_TOKEN] = (success, data) => window[CALLBACK_REVOKE_TOKEN] = (success, data) =>
success ? resolve(data) : reject(data); success ? resolve(data) : reject(data);
@ -68,7 +94,7 @@ export default class ExternalAuth extends Auth {
if (window.externalApp) { if (window.externalApp) {
window.externalApp.revokeExternalAuth(callbackPayload); window.externalApp.revokeExternalAuth(callbackPayload);
} else { } else {
window.webkit.messageHandlers.revokeExternalAuth.postMessage( window.webkit!.messageHandlers.revokeExternalAuth.postMessage(
callbackPayload callbackPayload
); );
} }

View File

@ -1,5 +1,18 @@
import { AuthData } from "home-assistant-js-websocket";
const storage = window.localStorage || {}; const storage = window.localStorage || {};
declare global {
interface Window {
__tokenCache: {
// undefined: we haven't loaded yet
// null: none stored
tokens?: AuthData | null;
writeEnabled?: boolean;
};
}
}
// So that core.js and main app hit same shared object. // So that core.js and main app hit same shared object.
let tokenCache = window.__tokenCache; let tokenCache = window.__tokenCache;
if (!tokenCache) { if (!tokenCache) {
@ -15,18 +28,22 @@ export function askWrite() {
); );
} }
export function saveTokens(tokens) { export function saveTokens(tokens: AuthData | null) {
tokenCache.tokens = tokens; tokenCache.tokens = tokens;
if (tokenCache.writeEnabled) { if (tokenCache.writeEnabled) {
try { try {
storage.hassTokens = JSON.stringify(tokens); storage.hassTokens = JSON.stringify(tokens);
} catch (err) {} // eslint-disable-line } catch (err) {
// write failed, ignore it. Happens if storage is full or private mode.
}
} }
} }
export function enableWrite() { export function enableWrite() {
tokenCache.writeEnabled = true; tokenCache.writeEnabled = true;
if (tokenCache.tokens) {
saveTokens(tokenCache.tokens); saveTokens(tokenCache.tokens);
}
} }
export function loadTokens() { export function loadTokens() {

View File

@ -1,4 +0,0 @@
/** Return if a component is loaded. */
export default function isComponentLoaded(hass, component) {
return hass && hass.config.components.indexOf(component) !== -1;
}

View File

@ -0,0 +1,9 @@
import { HomeAssistant } from "../../types";
/** Return if a component is loaded. */
export default function isComponentLoaded(
hass: HomeAssistant,
component: string
): boolean {
return hass && hass.config.components.indexOf(component) !== -1;
}

View File

@ -1,4 +1,4 @@
/** Return if the displaymode is in standalone mode (PWA). */ /** Return if the displaymode is in standalone mode (PWA). */
export default function isPwa() { export default function isPwa(): boolean {
return window.matchMedia("(display-mode: standalone)").matches; return window.matchMedia("(display-mode: standalone)").matches;
} }

View File

@ -1,4 +0,0 @@
/** Get the location name from a hass object. */
export default function computeLocationName(hass) {
return hass && hass.config.location_name;
}

View File

@ -0,0 +1,6 @@
import { HomeAssistant } from "../../types";
/** Get the location name from a hass object. */
export default function computeLocationName(hass: HomeAssistant): string {
return hass && hass.config.location_name;
}

View File

@ -1,4 +1,4 @@
export default function durationToSeconds(duration) { export default function durationToSeconds(duration: string): number {
const parts = duration.split(":").map(Number); const parts = duration.split(":").map(Number);
return parts[0] * 3600 + parts[1] * 60 + parts[2]; return parts[0] * 3600 + parts[1] * 60 + parts[2];
} }

View File

@ -11,11 +11,10 @@ function toLocaleDateStringSupportsOptions() {
} }
export default (toLocaleDateStringSupportsOptions() export default (toLocaleDateStringSupportsOptions()
? (dateObj, locales) => ? (dateObj: Date, locales: string) =>
dateObj.toLocaleDateString(locales, { dateObj.toLocaleDateString(locales, {
year: "numeric", year: "numeric",
month: "long", month: "long",
day: "numeric", day: "numeric",
}) })
: // eslint-disable-next-line no-unused-vars : (dateObj: Date) => fecha.format(dateObj, "mediumDate"));
(dateObj, locales) => fecha.format(dateObj, "mediumDate"));

View File

@ -11,7 +11,7 @@ function toLocaleStringSupportsOptions() {
} }
export default (toLocaleStringSupportsOptions() export default (toLocaleStringSupportsOptions()
? (dateObj, locales) => ? (dateObj: Date, locales: string) =>
dateObj.toLocaleString(locales, { dateObj.toLocaleString(locales, {
year: "numeric", year: "numeric",
month: "long", month: "long",
@ -19,5 +19,4 @@ export default (toLocaleStringSupportsOptions()
hour: "numeric", hour: "numeric",
minute: "2-digit", minute: "2-digit",
}) })
: // eslint-disable-next-line no-unused-vars : (dateObj: Date) => fecha.format(dateObj, "haDateTime"));
(dateObj, locales) => fecha.format(dateObj, "haDateTime"));

View File

@ -11,10 +11,9 @@ function toLocaleTimeStringSupportsOptions() {
} }
export default (toLocaleTimeStringSupportsOptions() export default (toLocaleTimeStringSupportsOptions()
? (dateObj, locales) => ? (dateObj: Date, locales: string) =>
dateObj.toLocaleTimeString(locales, { dateObj.toLocaleTimeString(locales, {
hour: "numeric", hour: "numeric",
minute: "2-digit", minute: "2-digit",
}) })
: // eslint-disable-next-line no-unused-vars : (dateObj: Date) => fecha.format(dateObj, "shortTime"));
(dateObj, locales) => fecha.format(dateObj, "shortTime"));

View File

@ -1,33 +0,0 @@
/** Calculate a string representing a date object as relative time from now.
*
* Example output: 5 minutes ago, in 3 days.
*/
const tests = [60, "second", 60, "minute", 24, "hour", 7, "day"];
export default function relativeTime(dateObj, localize) {
let delta = (new Date() - dateObj) / 1000;
const tense = delta >= 0 ? "past" : "future";
delta = Math.abs(delta);
for (let i = 0; i < tests.length; i += 2) {
if (delta < tests[i]) {
delta = Math.floor(delta);
const time = localize(
`ui.components.relative_time.duration.${tests[i + 1]}`,
"count",
delta
);
return localize(`ui.components.relative_time.${tense}`, "time", time);
}
delta /= tests[i];
}
delta = Math.floor(delta);
const time = localize(
"ui.components.relative_time.duration.week",
"count",
delta
);
return localize(`ui.components.relative_time.${tense}`, "time", time);
}

View File

@ -0,0 +1,40 @@
import { LocalizeFunc } from "../../mixins/localize-base-mixin";
/**
* Calculate a string representing a date object as relative time from now.
*
* Example output: 5 minutes ago, in 3 days.
*/
const tests = [60, 60, 24, 7];
const langKey = ["second", "minute", "hour", "day"];
export default function relativeTime(
dateObj: Date,
localize: LocalizeFunc
): string {
let delta = (new Date().getTime() - dateObj.getTime()) / 1000;
const tense = delta >= 0 ? "past" : "future";
delta = Math.abs(delta);
for (let i = 0; i < tests.length; i++) {
if (delta < tests[i]) {
delta = Math.floor(delta);
const timeDesc = localize(
`ui.components.relative_time.duration.${langKey[i]}`,
"count",
delta
);
return localize(`ui.components.relative_time.${tense}`, "time", timeDesc);
}
delta /= tests[i];
}
delta = Math.floor(delta);
const time = localize(
"ui.components.relative_time.duration.week",
"count",
delta
);
return localize(`ui.components.relative_time.${tense}`, "time", time);
}

View File

@ -1,6 +1,6 @@
const leftPad = (number) => (number < 10 ? `0${number}` : number); const leftPad = (num: number) => (num < 10 ? `0${num}` : num);
export default function secondsToDuration(d) { export default function secondsToDuration(d: number) {
const h = Math.floor(d / 3600); const h = Math.floor(d / 3600);
const m = Math.floor((d % 3600) / 60); const m = Math.floor((d % 3600) / 60);
const s = Math.floor((d % 3600) % 60); const s = Math.floor((d % 3600) % 60);

View File

@ -1,9 +0,0 @@
export default function attributeClassNames(stateObj, attributes) {
if (!stateObj) return "";
return attributes
.map(function(attribute) {
return attribute in stateObj.attributes ? "has-" + attribute : "";
})
.filter((attr) => attr !== "")
.join(" ");
}

View File

@ -0,0 +1,16 @@
import { HassEntity } from "home-assistant-js-websocket";
export default function attributeClassNames(
stateObj: HassEntity,
attributes: string[]
): string {
if (!stateObj) {
return "";
}
return attributes
.map((attribute) =>
attribute in stateObj.attributes ? "has-" + attribute : ""
)
.filter((attr) => attr !== "")
.join(" ");
}

View File

@ -1,7 +1,9 @@
import { HassEntity } from "home-assistant-js-websocket";
/** Return an icon representing a binary sensor state. */ /** Return an icon representing a binary sensor state. */
export default function binarySensorIcon(state) { export default function binarySensorIcon(state: HassEntity) {
var activated = state.state && state.state === "off"; const activated = state.state && state.state === "off";
switch (state.attributes.device_class) { switch (state.attributes.device_class) {
case "battery": case "battery":
return activated ? "hass:battery" : "hass:battery-outline"; return activated ? "hass:battery" : "hass:battery-outline";

View File

@ -1,4 +1,6 @@
export default function canToggleDomain(hass, domain) { import { HomeAssistant } from "../../types";
export default function canToggleDomain(hass: HomeAssistant, domain: string) {
const services = hass.services[domain]; const services = hass.services[domain];
if (!services) { if (!services) {
return false; return false;

View File

@ -1,13 +1,19 @@
import { HassEntity } from "home-assistant-js-websocket";
import canToggleDomain from "./can_toggle_domain"; import canToggleDomain from "./can_toggle_domain";
import computeStateDomain from "./compute_state_domain"; import computeStateDomain from "./compute_state_domain";
import { HomeAssistant } from "../../types";
export default function canToggleState(hass, stateObj) { export default function canToggleState(
hass: HomeAssistant,
stateObj: HassEntity
) {
const domain = computeStateDomain(stateObj); const domain = computeStateDomain(stateObj);
if (domain === "group") { if (domain === "group") {
return stateObj.state === "on" || stateObj.state === "off"; return stateObj.state === "on" || stateObj.state === "off";
} }
if (domain === "climate") { if (domain === "climate") {
return !!((stateObj.attributes || {}).supported_features & 4096); // tslint:disable-next-line
return (stateObj.attributes.supported_features! & 4096) !== 0;
} }
return canToggleDomain(hass, domain); return canToggleDomain(hass, domain);

View File

@ -1,3 +0,0 @@
export default function computeDomain(entityId) {
return entityId.substr(0, entityId.indexOf("."));
}

View File

@ -0,0 +1,3 @@
export default function computeDomain(entityId: string): string {
return entityId.substr(0, entityId.indexOf("."));
}

View File

@ -1,4 +1,4 @@
/** Compute the object ID of a state. */ /** Compute the object ID of a state. */
export default function computeObjectId(entityId) { export default function computeObjectId(entityId: string): string {
return entityId.substr(entityId.indexOf(".") + 1); return entityId.substr(entityId.indexOf(".") + 1);
} }

View File

@ -1,84 +0,0 @@
import computeStateDomain from "./compute_state_domain";
import formatDateTime from "../datetime/format_date_time";
import formatDate from "../datetime/format_date";
import formatTime from "../datetime/format_time";
export default function computeStateDisplay(localize, stateObj, language) {
if (!stateObj._stateDisplay) {
const domain = computeStateDomain(stateObj);
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
}`
);
}
if (!stateObj._stateDisplay) {
stateObj._stateDisplay = localize(
`state.${domain}.default.${stateObj.state}`
);
}
} else if (
stateObj.attributes.unit_of_measurement &&
!["unknown", "unavailable"].includes(stateObj.state)
) {
stateObj._stateDisplay =
stateObj.state + " " + stateObj.attributes.unit_of_measurement;
} else if (domain === "input_datetime") {
let date;
if (!stateObj.attributes.has_time) {
date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day
);
stateObj._stateDisplay = formatDate(date, language);
} else if (!stateObj.attributes.has_date) {
const now = new Date();
date = new Date(
// Due to bugs.chromium.org/p/chromium/issues/detail?id=797548
// don't use artificial 1970 year.
now.getFullYear(),
now.getMonth(),
now.getDay(),
stateObj.attributes.hour,
stateObj.attributes.minute
);
stateObj._stateDisplay = formatTime(date, language);
} else {
date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day,
stateObj.attributes.hour,
stateObj.attributes.minute
);
stateObj._stateDisplay = formatDateTime(date, language);
}
} else if (domain === "zwave") {
if (["initializing", "dead"].includes(stateObj.state)) {
stateObj._stateDisplay = localize(
`state.zwave.query_stage.${stateObj.state}`,
"query_stage",
stateObj.attributes.query_stage
);
} else {
stateObj._stateDisplay = localize(
`state.zwave.default.${stateObj.state}`
);
}
} else {
stateObj._stateDisplay = localize(`state.${domain}.${stateObj.state}`);
}
// Fall back to default, component backend translation, or raw state if nothing else matches.
stateObj._stateDisplay =
stateObj._stateDisplay ||
localize(`state.default.${stateObj.state}`) ||
localize(`component.${domain}.state.${stateObj.state}`) ||
stateObj.state;
}
return stateObj._stateDisplay;
}

View File

@ -0,0 +1,91 @@
import { HassEntity } from "home-assistant-js-websocket";
import computeStateDomain from "./compute_state_domain";
import formatDateTime from "../datetime/format_date_time";
import formatDate from "../datetime/format_date";
import formatTime from "../datetime/format_time";
import { LocalizeFunc } from "../../mixins/localize-base-mixin";
type CachedDisplayEntity = HassEntity & {
_stateDisplay?: string;
};
export default function computeStateDisplay(
localize: LocalizeFunc,
stateObj: HassEntity,
language: string
) {
const state = stateObj as CachedDisplayEntity;
if (!state._stateDisplay) {
const domain = computeStateDomain(state);
if (domain === "binary_sensor") {
// Try device class translation, then default binary sensor translation
if (state.attributes.device_class) {
state._stateDisplay = localize(
`state.${domain}.${state.attributes.device_class}.${state.state}`
);
}
if (!state._stateDisplay) {
state._stateDisplay = localize(
`state.${domain}.default.${state.state}`
);
}
} else if (
state.attributes.unit_of_measurement &&
!["unknown", "unavailable"].includes(state.state)
) {
state._stateDisplay =
state.state + " " + state.attributes.unit_of_measurement;
} else if (domain === "input_datetime") {
let date: Date;
if (!state.attributes.has_time) {
date = new Date(
state.attributes.year,
state.attributes.month - 1,
state.attributes.day
);
state._stateDisplay = formatDate(date, language);
} else if (!state.attributes.has_date) {
const now = new Date();
date = new Date(
// Due to bugs.chromium.org/p/chromium/issues/detail?id=797548
// don't use artificial 1970 year.
now.getFullYear(),
now.getMonth(),
now.getDay(),
state.attributes.hour,
state.attributes.minute
);
state._stateDisplay = formatTime(date, language);
} else {
date = new Date(
state.attributes.year,
state.attributes.month - 1,
state.attributes.day,
state.attributes.hour,
state.attributes.minute
);
state._stateDisplay = formatDateTime(date, language);
}
} else if (domain === "zwave") {
if (["initializing", "dead"].includes(state.state)) {
state._stateDisplay = localize(
`state.zwave.query_stage.${state.state}`,
"query_stage",
state.attributes.query_stage
);
} else {
state._stateDisplay = localize(`state.zwave.default.${state.state}`);
}
} else {
state._stateDisplay = localize(`state.${domain}.${state.state}`);
}
// Fall back to default, component backend translation, or raw state if nothing else matches.
state._stateDisplay =
state._stateDisplay ||
localize(`state.default.${state.state}`) ||
localize(`component.${domain}.state.${state.state}`) ||
state.state;
}
return state._stateDisplay;
}

View File

@ -1,5 +0,0 @@
import computeDomain from "./compute_domain";
export default function computeStateDomain(stateObj) {
return computeDomain(stateObj.entity_id);
}

View File

@ -0,0 +1,6 @@
import { HassEntity } from "home-assistant-js-websocket";
import computeDomain from "./compute_domain";
export default function computeStateDomain(stateObj: HassEntity) {
return computeDomain(stateObj.entity_id);
}

View File

@ -1,11 +0,0 @@
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, " ");
}
return stateObj._entityDisplay;
}

View File

@ -0,0 +1,18 @@
import { HassEntity } from "home-assistant-js-websocket";
import computeObjectId from "./compute_object_id";
type CachedDisplayEntity = HassEntity & {
_entityDisplay?: string;
};
export default function computeStateName(stateObj: HassEntity) {
const state = stateObj as CachedDisplayEntity;
if (state._entityDisplay === undefined) {
state._entityDisplay =
state.attributes.friendly_name ||
computeObjectId(state.entity_id).replace(/_/g, " ");
}
return state._entityDisplay;
}

View File

@ -1,8 +1,9 @@
/** Return an icon representing a cover state. */ /** Return an icon representing a cover state. */
import { HassEntity } from "home-assistant-js-websocket";
import domainIcon from "./domain_icon"; import domainIcon from "./domain_icon";
export default function coverIcon(state) { export default function coverIcon(state: HassEntity): string {
var open = state.state && state.state !== "closed"; const open = state.state !== "closed";
switch (state.attributes.device_class) { switch (state.attributes.device_class) {
case "garage": case "garage":
return open ? "hass:garage-open" : "hass:garage"; return open ? "hass:garage-open" : "hass:garage";

View File

@ -44,7 +44,7 @@ const fixedIcons = {
weblink: "hass:open-in-new", weblink: "hass:open-in-new",
}; };
export default function domainIcon(domain, state) { export default function domainIcon(domain: string, state?: string): string {
if (domain in fixedIcons) { if (domain in fixedIcons) {
return fixedIcons[domain]; return fixedIcons[domain];
} }
@ -93,11 +93,10 @@ export default function domainIcon(domain, state) {
} }
default: default:
/* eslint-disable no-console */ // tslint:disable-next-line
console.warn( console.warn(
"Unable to find icon for domain " + domain + " (" + state + ")" "Unable to find icon for domain " + domain + " (" + state + ")"
); );
/* eslint-enable no-console */
return DEFAULT_DOMAIN_ICON; return DEFAULT_DOMAIN_ICON;
} }
} }

View File

@ -0,0 +1,64 @@
import computeDomain from "./compute_domain";
export type FilterFunc = (entityId: string) => boolean;
export const generateFilter = (
includeDomains?: string[],
includeEntities?: string[],
excludeDomains?: string[],
excludeEntities?: string[]
): FilterFunc => {
const includeDomainsSet = new Set(includeDomains);
const includeEntitiesSet = new Set(includeEntities);
const excludeDomainsSet = new Set(excludeDomains);
const excludeEntitiesSet = new Set(excludeEntities);
const haveInclude = includeDomainsSet.size > 0 || includeEntitiesSet.size > 0;
const haveExclude = excludeDomainsSet.size > 0 || excludeEntitiesSet.size > 0;
// Case 1 - no includes or excludes - pass all entities
if (!haveInclude && !haveExclude) {
return () => true;
}
// Case 2 - includes, no excludes - only include specified entities
if (haveInclude && !haveExclude) {
return (entityId) =>
includeEntitiesSet.has(entityId) ||
includeDomainsSet.has(computeDomain(entityId));
}
// Case 3 - excludes, no includes - only exclude specified entities
if (!haveInclude && haveExclude) {
return (entityId) =>
!excludeEntitiesSet.has(entityId) &&
!excludeDomainsSet.has(computeDomain(entityId));
}
// Case 4 - both includes and excludes specified
// Case 4a - include domain specified
// - if domain is included, pass if entity not excluded
// - if domain is not included, pass if entity is included
// note: if both include and exclude domains specified,
// the exclude domains are ignored
if (includeDomainsSet.size) {
return (entityId) =>
includeDomainsSet.has(computeDomain(entityId))
? !excludeEntitiesSet.has(entityId)
: includeEntitiesSet.has(entityId);
}
// Case 4b - exclude domain specified
// - if domain is excluded, pass if entity is included
// - if domain is not excluded, pass if entity not excluded
if (excludeDomainsSet.size) {
return (entityId) =>
excludeDomainsSet.has(computeDomain(entityId))
? includeEntitiesSet.has(entityId)
: !excludeEntitiesSet.has(entityId);
}
// Case 4c - neither include or exclude domain specified
// - Only pass if entity is included. Ignore entity excludes.
return (entityId) => includeEntitiesSet.has(entityId);
};

View File

@ -1,8 +1,9 @@
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
import { DEFAULT_VIEW_ENTITY_ID } from "../const"; import { DEFAULT_VIEW_ENTITY_ID } from "../const";
// Return an ordered array of available views // Return an ordered array of available views
export default function extractViews(entities) { export default function extractViews(entities: HassEntities): HassEntity[] {
const views = []; const views: HassEntity[] = [];
Object.keys(entities).forEach((entityId) => { Object.keys(entities).forEach((entityId) => {
const entity = entities[entityId]; const entity = entities[entityId];

View File

@ -1,11 +0,0 @@
// Expects classNames to be an object mapping feature-bit -> className
export default function featureClassNames(stateObj, classNames) {
if (!stateObj || !stateObj.attributes.supported_features) return "";
const features = stateObj.attributes.supported_features;
return Object.keys(classNames)
.map((feature) => ((features & feature) !== 0 ? classNames[feature] : ""))
.filter((attr) => attr !== "")
.join(" ");
}

View File

@ -0,0 +1,21 @@
import { HassEntity } from "home-assistant-js-websocket";
// Expects classNames to be an object mapping feature-bit -> className
export default function featureClassNames(
stateObj: HassEntity,
classNames: { [feature: number]: string }
) {
if (!stateObj || !stateObj.attributes.supported_features) {
return "";
}
const features = stateObj.attributes.supported_features;
return Object.keys(classNames)
.map((feature) =>
// tslint:disable-next-line
(features & Number(feature)) !== 0 ? classNames[feature] : ""
)
.filter((attr) => attr !== "")
.join(" ");
}

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