Merge pull request #3134 from home-assistant/dev

20190427.0
This commit is contained in:
Paulus Schoutsen 2019-04-27 22:16:54 -07:00 committed by GitHub
commit c34dde815c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 788 additions and 214 deletions

View File

@ -1,6 +1,7 @@
import "@polymer/iron-icon/iron-icon";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-tooltip/paper-tooltip";
import "@polymer/paper-toggle-button/paper-toggle-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
@ -109,10 +110,18 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
margin: 16px 0;
display: block;
}
.state {
display: flex;
margin: 8px 0;
}
.state div {
width: 180px;
display: inline-block;
}
.state iron-icon {
width: 16px;
color: var(--secondary-text-color);
}
paper-toggle-button {
display: inline;
}
@ -156,6 +165,9 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
margin-right: 4px;
--iron-icon-height: 45px;
}
.protection-enable mwc-button {
--mdc-theme-primary: white;
}
</style>
<template is="dom-if" if="[[computeUpdateAvailable(addon)]]">
@ -188,6 +200,18 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
</paper-card>
</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 add-on 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 add-on.
</div>
<div class="card-actions protection-enable">
<mwc-button on-click="protectionToggled">Enable Protection mode</mwc-button>
</div>
</div>
</paper-card>
</template>
<paper-card>
<div class="card-content">
<div class="addon-header">
@ -226,22 +250,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
<img src="/api/hassio/addons/[[addonSlug]]/logo" />
</a>
</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 add-on 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 add-on.
</div>
<div class="card-actions">
<mwc-button on-click="protectionToggled">Enable Protection mode</mwc-button>
</div>
</div>
</paper-card>
</template>
<div class="security">
<h3>Add-on 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 add-on 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"
@ -348,22 +357,32 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
checked="[[addon.auto_update]]"
></paper-toggle-button>
</div>
<template is="dom-if" if="[[addon.ingress]]">
<div class="state">
<div>Show in sidebar</div>
<paper-toggle-button
on-change="panelToggled"
checked="[[addon.ingress_panel]]"
disabled="[[_computeCannotIngressSidebar(hass, addon)]]"
></paper-toggle-button>
<template is="dom-if" if="[[_computeCannotIngressSidebar(hass, addon)]]">
<span>This option requires Home Assistant 0.92 or later.</span>
</template>
</div>
</template>
<div class="state">
<div>Protection mode</div>
<div>
Protection mode
<span>
<iron-icon icon="hassio:information"></iron-icon>
<paper-tooltip>Grant the add-on elevated system access.</paper-tooltip>
</span>
</div>
<paper-toggle-button
on-change="protectionToggled"
checked="[[addon.protected]]"
></paper-toggle-button>
</div>
<template is="dom-if" if="[[addon.ingress]]">
<div class="state">
<div>Show in Sidebar</div>
<paper-toggle-button
on-change="panelToggled"
checked="[[addon.ingress_panel]]"
></paper-toggle-button>
</div>
</template>
</template>
</div>
<div class="card-actions">
@ -580,5 +599,14 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
this.fire("hass-api-called", eventData);
});
}
_computeCannotIngressSidebar(hass, addon) {
return !addon.ingress || !this._computeHA92plus(hass);
}
_computeHA92plus(hass) {
const [major, minor] = hass.config.version.split(".", 2);
return Number(major) > 0 || (major === "0" && Number(minor) >= 92);
}
}
customElements.define("hassio-addon-info", HassioAddonInfo);

View File

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

View File

@ -279,6 +279,14 @@ class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
// We have a new picture url
// If entity picture is non-relative, we use that url directly.
if (picture.substr(0, 1) !== "/") {
this._coverShowing = true;
this._coverLoadError = false;
this.$.cover.style.backgroundImage = `url(${picture})`;
return;
}
try {
const {
content_type: contentType,

View File

@ -0,0 +1 @@
export const stopPropagation = (ev) => ev.stopPropagation();

View File

@ -0,0 +1,126 @@
import {
html,
css,
LitElement,
TemplateResult,
property,
customElement,
} from "lit-element";
import "@polymer/paper-input/paper-input";
// tslint:disable-next-line:no-duplicate-imports
import { PaperInputElement } from "@polymer/paper-input/paper-input";
@customElement("ha-date-input")
export class HaDateInput extends LitElement {
@property() public year?: string;
@property() public month?: string;
@property() public day?: string;
@property({ type: Boolean }) public disabled = false;
static get styles() {
return css`
:host {
display: block;
font-family: var(--paper-font-common-base_-_font-family);
-webkit-font-smoothing: var(
--paper-font-common-base_-_-webkit-font-smoothing
);
}
paper-input {
width: 30px;
text-align: center;
--paper-input-container-input_-_-moz-appearance: textfield;
--paper-input-container-input-webkit-spinner_-_-webkit-appearance: none;
--paper-input-container-input-webkit-spinner_-_margin: 0;
--paper-input-container-input-webkit-spinner_-_display: none;
}
paper-input#year {
width: 50px;
}
.date-input-wrap {
display: flex;
flex-direction: row;
}
`;
}
protected render(): TemplateResult {
return html`
<div class="date-input-wrap">
<paper-input
id="year"
type="number"
.value=${this.year}
@change=${this._formatYear}
maxlength="4"
max="9999"
min="0"
.disabled=${this.disabled}
no-label-float
>
<span suffix="" slot="suffix">-</span>
</paper-input>
<paper-input
id="month"
type="number"
.value=${this.month}
@change=${this._formatMonth}
maxlength="2"
max="12"
min="1"
.disabled=${this.disabled}
no-label-float
>
<span suffix="" slot="suffix">-</span>
</paper-input>
<paper-input
id="day"
type="number"
.value=${this.day}
@change=${this._formatDay}
maxlength="2"
max="31"
min="1"
.disabled=${this.disabled}
no-label-float
>
</paper-input>
</div>
`;
}
private _formatYear() {
const yearElement = this.shadowRoot!.getElementById(
"year"
) as PaperInputElement;
this.year = yearElement.value!;
}
private _formatMonth() {
const monthElement = this.shadowRoot!.getElementById(
"month"
) as PaperInputElement;
this.month = ("0" + monthElement.value!).slice(-2);
}
private _formatDay() {
const dayElement = this.shadowRoot!.getElementById(
"day"
) as PaperInputElement;
this.day = ("0" + dayElement.value!).slice(-2);
}
get value() {
return `${this.year}-${this.month}-${this.day}`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-date-input": HaDateInput;
}
}

View File

@ -23,7 +23,7 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
class PaperTimeInput extends PolymerElement {
export class PaperTimeInput extends PolymerElement {
static get template() {
return html`
<style>

View File

@ -6,6 +6,12 @@ export interface FieldSchema {
optional: boolean;
}
export interface ConfigFlowProgress {
flow_id: string;
handler: string;
context: { [key: string]: any };
}
export interface ConfigFlowStepForm {
type: "form";
flow_id: string;
@ -62,3 +68,9 @@ export const handleConfigFlowStep = (
export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) =>
hass.callApi("DELETE", `config/config_entries/flow/${flowId}`);
export const getConfigFlowsInProgress = (hass: HomeAssistant) =>
hass.callApi<ConfigFlowProgress[]>("GET", "config/config_entries/flow");
export const getConfigFlowHandlers = (hass: HomeAssistant) =>
hass.callApi<string[]>("GET", "config/config_entries/flow_handlers");

View File

@ -1,6 +1,6 @@
import { HomeAssistant } from "../types";
export const setOption = (
export const setInputSelectOption = (
hass: HomeAssistant,
entity: string,
option: string

View File

@ -0,0 +1,11 @@
import { HomeAssistant } from "../types";
export const setInputDateTimeValue = (
hass: HomeAssistant,
entityId: string,
time: string | undefined = undefined,
date: string | undefined = undefined
) => {
const param = { entity_id: entityId, time, date };
hass.callService(entityId.split(".", 1)[0], "set_datetime", param);
};

View File

@ -23,13 +23,14 @@ import { HaPaperDialog } from "../../components/dialog/ha-paper-dialog";
import { haStyleDialog } from "../../resources/styles";
import {
fetchConfigFlow,
createConfigFlow,
ConfigFlowStep,
deleteConfigFlow,
getConfigFlowHandlers,
} from "../../data/config_entries";
import { PolymerChangedEvent } from "../../polymer-types";
import { HaConfigFlowParams } from "./show-dialog-config-flow";
import "./step-flow-pick-handler";
import "./step-flow-loading";
import "./step-flow-form";
import "./step-flow-abort";
@ -39,6 +40,7 @@ import {
fetchDeviceRegistry,
} from "../../data/device_registry";
import { AreaRegistryEntry, fetchAreaRegistry } from "../../data/area_registry";
import { HomeAssistant } from "../../types";
let instance = 0;
@ -47,12 +49,15 @@ declare global {
interface HASSDomEvents {
"flow-update": {
step?: ConfigFlowStep;
stepPromise?: Promise<ConfigFlowStep>;
};
}
}
@customElement("dialog-config-flow")
class ConfigFlowDialog extends LitElement {
public hass!: HomeAssistant;
@property()
private _params?: HaConfigFlowParams;
@ -62,7 +67,11 @@ class ConfigFlowDialog extends LitElement {
private _instance = instance;
@property()
private _step?: ConfigFlowStep;
private _step:
| ConfigFlowStep
| undefined
// Null means we need to pick a config flow
| null;
@property()
private _devices?: DeviceRegistryEntry[];
@ -70,25 +79,35 @@ class ConfigFlowDialog extends LitElement {
@property()
private _areas?: AreaRegistryEntry[];
@property()
private _handlers?: string[];
public async showDialog(params: HaConfigFlowParams): Promise<void> {
this._params = params;
this._loading = true;
this._instance = instance++;
const fetchStep = params.continueFlowId
? fetchConfigFlow(params.hass, params.continueFlowId)
: params.newFlowForHandler
? createConfigFlow(params.hass, params.newFlowForHandler)
: undefined;
// Create a new config flow. Show picker
if (!params.continueFlowId) {
this._step = null;
if (!fetchStep) {
throw new Error(`Pass in either continueFlowId or newFlorForHandler`);
// We only load the handlers once
if (this._handlers === undefined) {
this._loading = true;
this.updateComplete.then(() => this._scheduleCenterDialog());
try {
this._handlers = await getConfigFlowHandlers(this.hass);
} finally {
this._loading = false;
}
}
await this.updateComplete;
this._scheduleCenterDialog();
return;
}
this._loading = true;
const curInstance = this._instance;
await this.updateComplete;
const step = await fetchStep;
const step = await fetchConfigFlow(this.hass, params.continueFlowId);
// Happens if second showDialog called
if (curInstance !== this._instance) {
@ -99,7 +118,7 @@ class ConfigFlowDialog extends LitElement {
this._loading = false;
// When the flow changes, center the dialog.
// Don't do it on each step or else the dialog keeps bouncing.
setTimeout(() => this._dialog.center(), 0);
this._scheduleCenterDialog();
}
protected render(): TemplateResult | void {
@ -113,7 +132,7 @@ class ConfigFlowDialog extends LitElement {
opened
@opened-changed=${this._openedChanged}
>
${this._loading
${this._loading || (this._step === null && this._handlers === undefined)
? html`
<step-flow-loading></step-flow-loading>
`
@ -121,18 +140,26 @@ class ConfigFlowDialog extends LitElement {
? // When we are going to next step, we render 1 round of empty
// to reset the element.
""
: this._step === null
? // Show handler picker
html`
<step-flow-pick-handler
.hass=${this.hass}
.handlers=${this._handlers}
></step-flow-pick-handler>
`
: this._step.type === "form"
? html`
<step-flow-form
.step=${this._step}
.hass=${this._params.hass}
.hass=${this.hass}
></step-flow-form>
`
: this._step.type === "abort"
? html`
<step-flow-abort
.step=${this._step}
.hass=${this._params.hass}
.hass=${this.hass}
></step-flow-abort>
`
: this._devices === undefined || this._areas === undefined
@ -143,7 +170,7 @@ class ConfigFlowDialog extends LitElement {
: html`
<step-flow-create-entry
.step=${this._step}
.hass=${this._params.hass}
.hass=${this.hass}
.devices=${this._devices}
.areas=${this._areas}
></step-flow-create-entry>
@ -155,7 +182,8 @@ class ConfigFlowDialog extends LitElement {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.addEventListener("flow-update", (ev) => {
this._processStep((ev as any).detail.step);
const { step, stepPromise } = (ev as any).detail;
this._processStep(step || stepPromise);
});
}
@ -170,6 +198,10 @@ class ConfigFlowDialog extends LitElement {
}
}
private _scheduleCenterDialog() {
setTimeout(() => this._dialog.center(), 0);
}
private get _dialog(): HaPaperDialog {
return this.shadowRoot!.querySelector("ha-paper-dialog")!;
}
@ -177,17 +209,29 @@ class ConfigFlowDialog extends LitElement {
private async _fetchDevices(configEntryId) {
// Wait 5 seconds to give integrations time to find devices
await new Promise((resolve) => setTimeout(resolve, 5000));
const devices = await fetchDeviceRegistry(this._params!.hass);
const devices = await fetchDeviceRegistry(this.hass);
this._devices = devices.filter((device) =>
device.config_entries.includes(configEntryId)
);
}
private async _fetchAreas() {
this._areas = await fetchAreaRegistry(this._params!.hass);
this._areas = await fetchAreaRegistry(this.hass);
}
private async _processStep(step: ConfigFlowStep): Promise<void> {
private async _processStep(
step: ConfigFlowStep | undefined | Promise<ConfigFlowStep>
): Promise<void> {
if (step instanceof Promise) {
this._loading = true;
try {
this._step = await step;
} finally {
this._loading = false;
}
return;
}
if (step === undefined) {
this._flowDone();
return;
@ -206,8 +250,8 @@ class ConfigFlowDialog extends LitElement {
);
// If we created this flow, delete it now.
if (this._step && !flowFinished && this._params.newFlowForHandler) {
deleteConfigFlow(this._params.hass, this._step.flow_id);
if (this._step && !flowFinished && !this._params.continueFlowId) {
deleteConfigFlow(this.hass, this._step.flow_id);
}
this._params.dialogClosedCallback({
@ -221,8 +265,14 @@ class ConfigFlowDialog extends LitElement {
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
// Closed dialog by clicking on the overlay
if (this._step && !ev.detail.value) {
this._flowDone();
if (!ev.detail.value) {
if (this._step) {
this._flowDone();
} else if (this._step === null) {
// Flow aborted during picking flow
this._step = undefined;
this._params = undefined;
}
}
}

View File

@ -1,10 +1,7 @@
import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
export interface HaConfigFlowParams {
hass: HomeAssistant;
continueFlowId?: string;
newFlowForHandler?: string;
dialogClosedCallback: (params: { flowFinished: boolean }) => void;
}

View File

@ -167,6 +167,8 @@ class StepFlowForm extends LitElement {
toSendData
);
// make sure we're still showing the same step as when we
// fired off request.
if (!this.step || flowId !== this.step.flow_id) {
return;
}

View File

@ -0,0 +1,67 @@
import {
LitElement,
TemplateResult,
html,
css,
customElement,
CSSResult,
} from "lit-element";
import "@polymer/paper-spinner/paper-spinner-lite";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { HomeAssistant } from "../../types";
import { createConfigFlow } from "../../data/config_entries";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-icon-next";
@customElement("step-flow-pick-handler")
class StepFlowPickHandler extends LitElement {
public hass!: HomeAssistant;
public handlers!: string[];
protected render(): TemplateResult | void {
return html`
<h2>${this.hass.localize("ui.panel.config.integrations.new")}</h2>
<div>
${this.handlers.map(
(handler) =>
html`
<paper-item @click=${this._handlerPicked} .handler=${handler}>
<paper-item-body>
${this.hass.localize(`component.${handler}.config.title`)}
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
`
)}
</div>
`;
}
private async _handlerPicked(ev) {
fireEvent(this, "flow-update", {
stepPromise: createConfigFlow(this.hass, ev.currentTarget.handler),
});
}
static get styles(): CSSResult {
return css`
h2 {
padding-left: 16px;
}
div {
overflow: auto;
max-height: 600px;
}
paper-item {
cursor: pointer;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"step-flow-pick-handler": StepFlowPickHandler;
}
}

View File

@ -27,7 +27,7 @@ const isExternal = location.search.includes("external_auth=1");
const authProm = isExternal
? () =>
import(/* webpackChunkName: "external_auth" */ "../external_app/external_auth").then(
(mod) => new mod.default(hassUrl)
({ createExternalAuth }) => createExternalAuth(hassUrl)
)
: () =>
getAuth({

View File

@ -45,10 +45,10 @@ if (!window.externalApp && !window.webkit) {
);
}
export default class ExternalAuth extends Auth {
public external = new ExternalMessaging();
class ExternalAuth extends Auth {
public external?: ExternalMessaging;
constructor(hassUrl) {
constructor(hassUrl: string) {
super({
hassUrl,
clientId: "",
@ -58,7 +58,6 @@ export default class ExternalAuth extends Auth {
// This will trigger connection to do a refresh right away
expires: 0,
});
this.external.attach();
}
public async refreshAccessToken() {
@ -100,3 +99,15 @@ export default class ExternalAuth extends Auth {
});
}
}
export const createExternalAuth = (hassUrl: string) => {
const auth = new ExternalAuth(hassUrl);
if (
(window.externalApp && window.externalApp.externalBus) ||
(window.webkit && window.webkit.messageHandlers.externalBus)
) {
auth.external = new ExternalMessaging();
auth.external.attach();
}
return auth;
};

View File

@ -1,6 +1,7 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/paper-tooltip/paper-tooltip";
import "@material/mwc-button";
import "@polymer/paper-fab/paper-fab";
import "@polymer/paper-card/paper-card";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-item/paper-item";
@ -13,6 +14,7 @@ import "../../../layouts/hass-subpage";
import "../../../resources/ha-style";
import "../../../components/ha-icon-next";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../ha-config-section";
import EventsMixin from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
@ -50,6 +52,28 @@ class HaConfigManagerDashboard extends LocalizeMixin(
color: var(--primary-text-color);
text-decoration: none;
}
paper-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
paper-fab[is-wide] {
bottom: 24px;
right: 24px;
}
paper-fab[rtl] {
right: auto;
left: 16px;
}
paper-fab[rtl][is-wide] {
bottom: 24px;
right: auto;
left: 24px;
}
</style>
<hass-subpage
@ -119,23 +143,13 @@ class HaConfigManagerDashboard extends LocalizeMixin(
</paper-card>
</ha-config-section>
<ha-config-section>
<span slot="header"
>[[localize('ui.panel.config.integrations.new')]]</span
>
<paper-card>
<template is="dom-repeat" items="[[handlers]]">
<div class="config-entry-row">
<paper-item-body>
[[_computeIntegrationTitle(localize, item)]]
</paper-item-body>
<mwc-button on-click="_createFlow"
>[[localize('ui.panel.config.integrations.configure')]]</mwc-button
>
</div>
</template>
</paper-card>
</ha-config-section>
<paper-fab
icon="hass:plus"
title="[[localize('ui.panel.config.integrations.new')]]"
on-click="_createFlow"
is-wide$="[[isWide]]"
rtl$="[[rtl]]"
></paper-fab>
</hass-subpage>
`;
}
@ -162,6 +176,12 @@ class HaConfigManagerDashboard extends LocalizeMixin(
progress: Array,
handlers: Array,
rtl: {
type: Boolean,
reflectToAttribute: true,
computed: "_computeRTL(hass)",
},
};
}
@ -170,17 +190,14 @@ class HaConfigManagerDashboard extends LocalizeMixin(
loadConfigFlowDialog();
}
_createFlow(ev) {
_createFlow() {
showConfigFlowDialog(this, {
hass: this.hass,
newFlowForHandler: ev.model.item,
dialogClosedCallback: () => this.fire("hass-reload-entries"),
});
}
_continueFlow(ev) {
showConfigFlowDialog(this, {
hass: this.hass,
continueFlowId: ev.model.item.flow_id,
dialogClosedCallback: () => this.fire("hass-reload-entries"),
});
@ -230,6 +247,10 @@ class HaConfigManagerDashboard extends LocalizeMixin(
_handleMoreInfo(ev) {
this.fire("hass-more-info", { entityId: ev.model.item.entity_id });
}
_computeRTL(hass) {
return computeRTL(hass);
}
}
customElements.define("ha-config-entries-dashboard", HaConfigManagerDashboard);

View File

@ -11,6 +11,10 @@ interface Config extends LovelaceElementConfig {
}
export const computeTooltip = (hass: HomeAssistant, config: Config): string => {
if (config.title === null) {
return "";
}
if (config.title) {
return config.title;
}
@ -26,10 +30,10 @@ export const computeTooltip = (hass: HomeAssistant, config: Config): string => {
}
const tapTooltip = config.tap_action
? computeActionTooltip(stateName, config.tap_action, false)
? computeActionTooltip(hass, stateName, config.tap_action, false)
: "";
const holdTooltip = config.hold_action
? computeActionTooltip(stateName, config.hold_action, true)
? computeActionTooltip(hass, stateName, config.hold_action, true)
: "";
const newline = tapTooltip && holdTooltip ? "\n" : "";
@ -40,6 +44,7 @@ export const computeTooltip = (hass: HomeAssistant, config: Config): string => {
};
function computeActionTooltip(
hass: HomeAssistant,
state: string,
config: ActionConfig,
isHold: boolean
@ -48,20 +53,39 @@ function computeActionTooltip(
return "";
}
let tooltip = isHold ? "Hold: " : "Tap: ";
let tooltip =
(isHold
? hass.localize("ui.panel.lovelace.cards.picture-elements.hold")
: hass.localize("ui.panel.lovelace.cards.picture-elements.tap")) + " ";
switch (config.action) {
case "navigate":
tooltip += `Navigate to ${config.navigation_path}`;
tooltip += `${hass.localize(
"ui.panel.lovelace.cards.picture-elements.navigate_to",
"location",
config.navigation_path
)}`;
break;
case "toggle":
tooltip += `Toggle ${state}`;
tooltip += `${hass.localize(
"ui.panel.lovelace.cards.picture-elements.toggle",
"name",
state
)}`;
break;
case "call-service":
tooltip += `Call service ${config.service}`;
tooltip += `${hass.localize(
"ui.panel.lovelace.cards.picture-elements.call_service",
"name",
config.service
)}`;
break;
case "more-info":
tooltip += `Show more-info: ${state}`;
tooltip += `${hass.localize(
"ui.panel.lovelace.cards.picture-elements.more_info",
"name",
state
)}`;
break;
}

View File

@ -10,6 +10,7 @@ import {
import "../entity-rows/hui-climate-entity-row";
import "../entity-rows/hui-cover-entity-row";
import "../entity-rows/hui-group-entity-row";
import "../entity-rows/hui-input-datetime-entity-row";
import "../entity-rows/hui-input-number-entity-row";
import "../entity-rows/hui-input-select-entity-row";
import "../entity-rows/hui-input-text-entity-row";
@ -58,6 +59,7 @@ const DOMAIN_TO_ELEMENT_TYPE = {
// Temporary. Once climate is rewritten,
// water heater should get it's own row.
water_heater: "climate",
input_datetime: "input-datetime",
};
const TIMEOUT = 2000;

View File

@ -62,6 +62,7 @@ export class HuiStateIconElement extends LitElement implements LovelaceElement {
@ha-click="${this._handleClick}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
.overrideIcon=${this._config.icon}
></state-badge>
`;
}

View File

@ -51,6 +51,7 @@ export interface StateIconElementConfig extends LovelaceElementConfig {
entity: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
icon?: string;
}
export interface StateLabelElementConfig extends LovelaceElementConfig {

View File

@ -0,0 +1,128 @@
import {
html,
LitElement,
TemplateResult,
property,
PropertyValues,
customElement,
} from "lit-element";
import "../components/hui-generic-entity-row";
import "../../../components/paper-time-input.js";
// tslint:disable-next-line:no-duplicate-imports
import { PaperTimeInput } from "../../../components/paper-time-input.js";
import "../../../components/ha-date-input";
// tslint:disable-next-line:no-duplicate-imports
import { HaDateInput } from "../../../components/ha-date-input";
import { HomeAssistant } from "../../../types";
import { EntityRow, EntityConfig } from "./types";
import { setInputDateTimeValue } from "../../../data/input_datetime";
import { hasConfigOrEntityChanged } from "../common/has-changed";
@customElement("hui-input-datetime-entity-row")
class HuiInputDatetimeEntityRow extends LitElement implements EntityRow {
@property() public hass?: HomeAssistant;
@property() private _config?: EntityConfig;
public setConfig(config: EntityConfig): void {
if (!config) {
throw new Error("Configuration error");
}
this._config = config;
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
return hasConfigOrEntityChanged(this, changedProps);
}
protected render(): TemplateResult | void {
if (!this._config || !this.hass) {
return html``;
}
const stateObj = this.hass.states[this._config.entity];
if (!stateObj) {
return html`
<hui-warning
>${this.hass.localize(
"ui.panel.lovelace.warning.entity_not_found",
"entity",
this._config.entity
)}</hui-warning
>
`;
}
return html`
<hui-generic-entity-row .hass="${this.hass}" .config="${this._config}">
${stateObj.attributes.has_date
? html`
<ha-date-input
.year=${stateObj.attributes.year}
.month=${("0" + stateObj.attributes.month).slice(-2)}
.day=${("0" + stateObj.attributes.day).slice(-2)}
@change=${this._selectedValueChanged}
@click=${this._stopEventPropagation}
></ha-date-input>
${stateObj.attributes.has_time ? "," : ""}
`
: ``}
${stateObj.attributes.has_time
? html`
<paper-time-input
.hour=${stateObj.state === "unknown"
? ""
: ("0" + stateObj.attributes.hour).slice(-2)}
.min=${stateObj.state === "unknown"
? ""
: ("0" + stateObj.attributes.minute).slice(-2)}
.amPm=${false}
@change=${this._selectedValueChanged}
@click=${this._stopEventPropagation}
hide-label
format="24"
></paper-time-input>
`
: ``}
</hui-generic-entity-row>
`;
}
private _stopEventPropagation(ev: Event): void {
ev.stopPropagation();
}
private get _timeInputEl(): PaperTimeInput {
return this.shadowRoot!.querySelector("paper-time-input")!;
}
private get _dateInputEl(): HaDateInput {
return this.shadowRoot!.querySelector("ha-date-input")!;
}
private _selectedValueChanged(ev): void {
const stateObj = this.hass!.states[this._config!.entity];
const time =
this._timeInputEl !== null
? this._timeInputEl.value.trim() + ":00"
: undefined;
const date =
this._dateInputEl !== null ? this._dateInputEl.value : undefined;
if (time !== stateObj.state) {
setInputDateTimeValue(this.hass!, stateObj.entity_id, time, date);
}
ev.target.blur();
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-input-datetime-entity-row": HuiInputDatetimeEntityRow;
}
}

View File

@ -8,7 +8,6 @@ import {
customElement,
PropertyValues,
} from "lit-element";
import { repeat } from "lit-html/directives/repeat";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
@ -18,11 +17,12 @@ import "../components/hui-warning";
import computeStateName from "../../../common/entity/compute_state_name";
import { HomeAssistant } from "../../../types";
import { HomeAssistant, InputSelectEntity } from "../../../types";
import { EntityRow, EntityConfig } from "./types";
import { setOption } from "../../../data/input-select";
import { setInputSelectOption } from "../../../data/input-select";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { forwardHaptic } from "../../../util/haptics";
import { stopPropagation } from "../../../common/dom/stop_propagation";
@customElement("hui-input-select-entity-row")
class HuiInputSelectEntityRow extends LitElement implements EntityRow {
@ -47,7 +47,9 @@ class HuiInputSelectEntityRow extends LitElement implements EntityRow {
return html``;
}
const stateObj = this.hass.states[this._config.entity];
const stateObj = this.hass.states[this._config.entity] as
| InputSelectEntity
| undefined;
if (!stateObj) {
return html`
@ -64,26 +66,43 @@ class HuiInputSelectEntityRow extends LitElement implements EntityRow {
return html`
<state-badge .stateObj="${stateObj}"></state-badge>
<ha-paper-dropdown-menu
selected-item-label="${stateObj.state}"
@selected-item-label-changed="${this._selectedChanged}"
label="${this._config.name || computeStateName(stateObj)}"
.label=${this._config.name || computeStateName(stateObj)}
.value=${stateObj.state}
@iron-select=${this._selectedChanged}
@click=${stopPropagation}
>
<paper-listbox
slot="dropdown-content"
selected="${stateObj.attributes.options.indexOf(stateObj.state)}"
>
${repeat(
stateObj.attributes.options,
(option) =>
html`
<paper-item>${option}</paper-item>
`
<paper-listbox slot="dropdown-content">
${stateObj.attributes.options.map(
(option) => html`
<paper-item>${option}</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
`;
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (!this.hass || !this._config) {
return;
}
const stateObj = this.hass.states[this._config.entity] as
| InputSelectEntity
| undefined;
if (!stateObj) {
return;
}
// Update selected after rendering the items or else it won't work in Firefox
this.shadowRoot!.querySelector(
"paper-listbox"
)!.selected = stateObj.attributes.options.indexOf(stateObj.state);
}
static get styles(): CSSResult {
return css`
:host {
@ -94,22 +113,28 @@ class HuiInputSelectEntityRow extends LitElement implements EntityRow {
margin-left: 16px;
flex: 1;
}
paper-item {
cursor: pointer;
min-width: 200px;
}
`;
}
private _selectedChanged(ev): void {
forwardHaptic(this, "light");
// Selected Option will transition to '' before transitioning to new value
const stateObj = this.hass!.states[this._config!.entity];
if (
!ev.target.selectedItem ||
ev.target.selectedItem.innerText === "" ||
ev.target.selectedItem.innerText === stateObj.state
) {
const option = ev.detail.item.innerText;
if (option === stateObj.state) {
return;
}
setOption(this.hass!, stateObj.entity_id, ev.target.selectedItem.innerText);
forwardHaptic(this, "light");
setInputSelectOption(
this.hass!,
stateObj.entity_id,
ev.target.selectedItem.innerText
);
}
}

View File

@ -18,6 +18,12 @@ export interface PolymerChangedEvent<T> extends Event {
};
}
export interface PolymerIronSelectEvent<T> extends Event {
detail: {
item: T;
};
}
declare global {
// for fire event
interface HASSDomEvents {

View File

@ -1,96 +0,0 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/entity/state-badge";
import computeStateName from "../common/entity/compute_state_name";
class StateCardInputSelect extends PolymerElement {
static get template() {
return html`
<style>
:host {
display: block;
}
state-badge {
float: left;
margin-top: 10px;
}
paper-dropdown-menu {
display: block;
margin-left: 53px;
}
paper-item {
cursor: pointer;
}
</style>
${this.stateBadgeTemplate}
<paper-dropdown-menu
on-click="stopPropagation"
selected-item-label="{{selectedOption}}"
label="[[_computeStateName(stateObj)]]"
>
<paper-listbox
slot="dropdown-content"
selected="[[computeSelected(stateObj)]]"
>
<template is="dom-repeat" items="[[stateObj.attributes.options]]">
<paper-item>[[item]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
`;
}
static get stateBadgeTemplate() {
return html`
<state-badge state-obj="[[stateObj]]"></state-badge>
`;
}
static get properties() {
return {
hass: Object,
stateObj: Object,
inDialog: {
type: Boolean,
value: false,
},
selectedOption: {
type: String,
observer: "selectedOptionChanged",
},
};
}
_computeStateName(stateObj) {
return computeStateName(stateObj);
}
computeSelected(stateObj) {
return stateObj.attributes.options.indexOf(stateObj.state);
}
selectedOptionChanged(option) {
// Selected Option will transition to '' before transitioning to new value
if (option === "" || option === this.stateObj.state) {
return;
}
this.hass.callService("input_select", "select_option", {
option: option,
entity_id: this.stateObj.entity_id,
});
}
stopPropagation(ev) {
ev.stopPropagation();
}
}
customElements.define("state-card-input_select", StateCardInputSelect);

View File

@ -0,0 +1,96 @@
import {
LitElement,
customElement,
TemplateResult,
html,
CSSResult,
css,
property,
PropertyValues,
} from "lit-element";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@polymer/paper-item/paper-item";
// tslint:disable-next-line: no-duplicate-imports
import { PaperItemElement } from "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "../components/entity/state-badge";
import computeStateName from "../common/entity/compute_state_name";
import { HomeAssistant, InputSelectEntity } from "../types";
import { setInputSelectOption } from "../data/input-select";
import { PolymerIronSelectEvent } from "../polymer-types";
import { stopPropagation } from "../common/dom/stop_propagation";
@customElement("state-card-input_select")
class StateCardInputSelect extends LitElement {
@property() public hass!: HomeAssistant;
@property() public stateObj!: InputSelectEntity;
protected render(): TemplateResult | void {
return html`
<state-badge .stateObj=${this.stateObj}></state-badge>
<paper-dropdown-menu-light
.label=${computeStateName(this.stateObj)}
.value="${this.stateObj.state}"
@iron-select=${this._selectedOptionChanged}
@click=${stopPropagation}
>
<paper-listbox slot="dropdown-content">
${this.stateObj.attributes.options.map(
(option) => html`
<paper-item>${option}</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
`;
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
// Update selected after rendering the items or else it won't work in Firefox
this.shadowRoot!.querySelector(
"paper-listbox"
)!.selected = this.stateObj.attributes.options.indexOf(this.stateObj.state);
}
private async _selectedOptionChanged(
ev: PolymerIronSelectEvent<PaperItemElement>
) {
const option = ev.detail.item.innerText;
if (option === this.stateObj.state) {
return;
}
await setInputSelectOption(this.hass, this.stateObj.entity_id, option);
}
static get styles(): CSSResult {
return css`
:host {
display: block;
}
state-badge {
float: left;
margin-top: 10px;
}
paper-dropdown-menu-light {
display: block;
margin-left: 53px;
}
paper-item {
cursor: pointer;
min-width: 200px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"state-card-input_select": StateCardInputSelect;
}
}

View File

@ -916,6 +916,14 @@
"checked_items": "Checked items",
"clear_items": "Clear checked items",
"add_item": "Add item"
},
"picture-elements": {
"hold": "Hold:",
"tap": "Tap:",
"navigate_to": "Navigate to {location}",
"toggle": "Toggle {name}",
"call_service": "Call service {name}",
"more_info": "Show more-info: {name}"
}
},
"menu": {

View File

@ -219,6 +219,12 @@ export type CameraEntity = HassEntityBase & {
};
};
export type InputSelectEntity = HassEntityBase & {
attributes: HassEntityAttributeBase & {
options: string[];
};
};
export interface Route {
prefix: string;
path: string;

View File

@ -629,6 +629,9 @@
"password": "Cyfrinair",
"create": "Creu"
}
},
"cloud": {
"description_features": "Rheolaeth oddi cartref, integreiddio gyda Alexa a Google Assistant."
}
},
"lovelace": {

View File

@ -178,7 +178,7 @@
"stopped": "Σταμάτησε",
"locked": "Κλειδωμένο",
"unlocked": "Ξεκλείδωτο",
"ok": "Ένταξη",
"ok": "Εντάξει",
"problem": "Πρόβλημα"
},
"input_boolean": {

View File

@ -581,7 +581,8 @@
"cloud": {
"caption": "Home Assistant Felhő",
"description_login": "Bejelentkezve mint {email}",
"description_not_login": "Nincs bejelentkezve"
"description_not_login": "Nincs bejelentkezve",
"description_features": "Távoli vezérlés, Alexa és Google Assistant integráció"
},
"integrations": {
"caption": "Integrációk",

View File

@ -101,13 +101,24 @@
"gas": "Gas",
"manual": "Handvirkt"
},
"cover": {
"open": "Opin",
"opening": "Opna",
"closed": "Lokað",
"closing": "Loka"
},
"device_tracker": {
"home": "Heima",
"not_home": "Fjarverandi"
},
"group": {
"off": "Óvirkur",
"on": "Virkur",
"home": "Heima",
"not_home": "Fjarverandi",
"open": "Opin",
"opening": "Opna",
"closed": "Lokuð",
"closing": "Loka",
"stopped": "Stöðvað",
"locked": "Læst",
@ -172,6 +183,7 @@
"rainy": "Rigning",
"snowy": "Snjókoma",
"snowy-rainy": "Slydda",
"sunny": "Sólskin",
"windy": "Vindasamt",
"windy-variant": "Vindasamt"
},
@ -229,12 +241,16 @@
},
"config": {
"header": "Stilla af Home Assistant",
"introduction": "Hér er mögulegt að stilla af íhluti og Home Assistang. Því miður er ekki hægt að breyta öllu í gegnum viðmótið ennþá, en við erum að vinna í því.",
"core": {
"caption": "Almennt",
"description": "Staðfesta að stillingarskráin þín sé rétt og stjórnun á miðlara",
"section": {
"core": {
"header": "Stillingar og stjórnun þjóns",
"introduction": "Að breyta stillingum getur verið þreytandi ferli og við vitum það. Þetta svæði mun reyna að létta þér lífið hvað það varðar.",
"validation": {
"heading": "Staðfesta stillingar",
"check_config": "Athuga stillingar",
"valid": "Stillingar í lagi!",
"invalid": "Stillingar ógildar"
@ -243,7 +259,8 @@
"heading": "Endurhleðsla stillinga",
"core": "Endurhlaða inn kjarna",
"group": "Endurhlaða inn hópum",
"automation": "Endurhlaða inn sjálfvirkni"
"automation": "Endurhlaða inn sjálfvirkni",
"script": "Endurhlaða inn skriftum"
},
"server_management": {
"heading": "Stjórnun miðlara",
@ -256,6 +273,7 @@
},
"customize": {
"caption": "Séraðlögun",
"description": "Séraðlögun fyrir einingarnar þínar",
"picker": {
"header": "Séraðlögun"
}
@ -265,6 +283,7 @@
"description": "Stofna og breyta sjálfvirkni",
"picker": {
"pick_automation": "Veldu sjálfvirkni sem á að breyta",
"no_automations": "Við fundum ekki neinar sjálfvirkni-skilgreiningar sem hægt er að breyta",
"add_automation": "Bæta við sjálfvirkni",
"learn_more": "Læra meira um sjálfvirkni"
},
@ -300,6 +319,10 @@
"mqtt": {
"label": "MQTT"
},
"numeric_state": {
"above": "Yfir",
"below": "Undir"
},
"sun": {
"label": "Sól",
"event": "Viðburður:",
@ -348,6 +371,10 @@
"label": "Staða",
"state": "Staða"
},
"numeric_state": {
"above": "Yfir",
"below": "Undir"
},
"sun": {
"label": "Sól",
"before": "Fyrir:",
@ -656,6 +683,7 @@
}
},
"page-onboarding": {
"intro": "Ertu tilbúinn að vekja heimilið þitt, endurheimta friðhelgi þína og gerast þáttakandi í samfélagi grúskara á heimsvísu?",
"user": {
"intro": "Hefjumst handa með því að byrja á að stona notanda aðgang.",
"required_field": "Skilyrt",
@ -896,6 +924,7 @@
"confirm": "Vista innskráningu"
},
"notification_drawer": {
"click_to_configure": "Smelltu á hnappinn til að stilla {entity}",
"empty": "Engar tilkynningar",
"title": "Tilkynningar"
}
@ -904,11 +933,16 @@
"automation": "Sjálfvirkni",
"calendar": "Dagatal",
"camera": "Myndavél",
"climate": "Loftslag",
"configurator": "Stillingarálfur",
"conversation": "Samtal",
"cover": "Gluggatjöld",
"fan": "Vifta",
"group": "Hópur",
"input_datetime": "Innsláttar dagsetning\/tími",
"input_select": "Innsláttarval",
"input_number": "Innsláttarnúmer",
"input_text": "Innsláttartexti",
"light": "Ljós",
"lock": "Lás",
"mailbox": "Pósthólf",
@ -917,6 +951,7 @@
"plant": "Planta",
"proximity": "Nálægð",
"scene": "Sena",
"script": "Skrifta",
"sensor": "Skynjari",
"sun": "Sól",
"switch": "Rofi",