Merge pull request #4193 from home-assistant/dev

20191108.0
This commit is contained in:
Bram Kragten 2019-11-08 17:41:58 +01:00 committed by GitHub
commit bc01df42d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
130 changed files with 4864 additions and 1292 deletions

View File

@ -28,5 +28,5 @@ module.exports = {
hassio_dir: path.resolve(__dirname, "../hassio"),
hassio_root: path.resolve(__dirname, "../hassio/build"),
hassio_publicPath: "/api/hassio/app",
hassio_publicPath: "/api/hassio/app/",
};

View File

@ -161,8 +161,8 @@ if (!window.cardTools) {
};
cardTools.longpress = (element) => {
customElements.whenDefined("long-press").then(() => {
const longpress = document.body.querySelector("long-press");
customElements.whenDefined("action-handler").then(() => {
const longpress = document.body.querySelector("action-handler");
longpress.bind(element);
});
return element;

View File

@ -2,7 +2,8 @@ import { html, LitElement, TemplateResult } from "lit-element";
import "@material/mwc-button";
import "../../../src/components/ha-card";
import { longPress } from "../../../src/panels/lovelace/common/directives/long-press-directive";
import { actionHandler } from "../../../src/panels/lovelace/common/directives/action-handler-directive";
import { ActionHandlerEvent } from "../../../src/data/lovelace";
export class DemoUtilLongPress extends LitElement {
protected render(): TemplateResult | void {
@ -12,9 +13,8 @@ export class DemoUtilLongPress extends LitElement {
() => html`
<ha-card>
<mwc-button
@ha-click="${this._handleClick}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
@action=${this._handleAction}
.actionHandler=${actionHandler({})}
>
(long) press me!
</mwc-button>
@ -28,12 +28,8 @@ export class DemoUtilLongPress extends LitElement {
`;
}
private _handleClick(ev: Event) {
this._addValue(ev, "tap");
}
private _handleHold(ev: Event) {
this._addValue(ev, "hold");
private _handleAction(ev: ActionHandlerEvent) {
this._addValue(ev, ev.detail.action!);
}
private _addValue(ev: Event, value: string) {

View File

@ -122,6 +122,7 @@
"@types/leaflet": "^1.4.3",
"@types/memoize-one": "4.1.0",
"@types/mocha": "^5.2.6",
"@types/webspeechapi": "^0.0.29",
"babel-loader": "^8.0.5",
"chai": "^4.2.0",
"copy-webpack-plugin": "^5.0.2",

View File

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

View File

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

View File

@ -21,6 +21,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
computeDeviceName,
} from "../../data/device_registry";
import { compare } from "../../common/string/compare";
import { PolymerChangedEvent } from "../../polymer-types";
@ -33,7 +34,6 @@ import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import { computeStateName } from "../../common/entity/compute_state_name";
interface Device {
name: string;
@ -102,11 +102,11 @@ class HaDevicePicker extends SubscribeMixin(LitElement) {
const outputDevices = devices.map((device) => {
return {
id: device.id,
name:
device.name_by_user ||
device.name ||
this._fallbackDeviceName(device.id, deviceEntityLookup) ||
"No name",
name: computeDeviceName(
device,
this.hass,
deviceEntityLookup[device.id]
),
area: device.area_id ? areaLookup[device.area_id].name : "No area",
};
});
@ -209,20 +209,6 @@ class HaDevicePicker extends SubscribeMixin(LitElement) {
}
}
private _fallbackDeviceName(
deviceId: string,
deviceEntityLookup: DeviceEntityLookup
): string | undefined {
for (const entity of deviceEntityLookup[deviceId] || []) {
const stateObj = this.hass.states[entity.entity_id];
if (stateObj) {
return computeStateName(stateObj);
}
}
return undefined;
}
static get styles(): CSSResult {
return css`
paper-input > paper-icon-button {

View File

@ -22,7 +22,20 @@ import { HassEntity } from "home-assistant-js-websocket";
class HaEntitiesPickerLight extends LitElement {
@property() public hass?: HomeAssistant;
@property() public value?: string[];
@property({ attribute: "domain-filter" }) public domainFilter?: string;
/**
* Show entities from specific domains.
* @type {string}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show no entities of these domains.
* @type {Array}
* @attr exclude-domains
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
@property({ attribute: "picked-entity-label" })
public pickedEntityLabel?: string;
@property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string;
@ -31,6 +44,7 @@ class HaEntitiesPickerLight extends LitElement {
if (!this.hass) {
return;
}
const currentEntities = this._currentEntities;
return html`
${currentEntities.map(
@ -40,7 +54,8 @@ class HaEntitiesPickerLight extends LitElement {
allow-custom-entity
.curValue=${entityId}
.hass=${this.hass}
.domainFilter=${this.domainFilter}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.entityFilter=${this._entityFilter}
.value=${entityId}
.label=${this.pickedEntityLabel}
@ -52,7 +67,8 @@ class HaEntitiesPickerLight extends LitElement {
<div>
<ha-entity-picker
.hass=${this.hass}
.domainFilter=${this.domainFilter}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.entityFilter=${this._entityFilter}
.label=${this.pickEntityLabel}
@value-changed=${this._addEntity}

View File

@ -60,7 +60,20 @@ class HaEntityPicker extends LitElement {
@property() public hass?: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property({ attribute: "domain-filter" }) public domainFilter?: string;
/**
* Show entities from specific domains.
* @type {string}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show no entities of these domains.
* @type {Array}
* @attr exclude-domains
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Boolean }) private _opened?: boolean;
@property() private _hass?: HomeAssistant;
@ -68,7 +81,8 @@ class HaEntityPicker extends LitElement {
private _getStates = memoizeOne(
(
hass: this["hass"],
domainFilter: this["domainFilter"],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
entityFilter: this["entityFilter"]
) => {
let states: HassEntity[] = [];
@ -78,9 +92,15 @@ class HaEntityPicker extends LitElement {
}
let entityIds = Object.keys(hass.states);
if (domainFilter) {
if (includeDomains) {
entityIds = entityIds.filter((eid) =>
includeDomains.includes(eid.substr(0, eid.indexOf(".")))
);
}
if (excludeDomains) {
entityIds = entityIds.filter(
(eid) => eid.substr(0, eid.indexOf(".")) === domainFilter
(eid) => !excludeDomains.includes(eid.substr(0, eid.indexOf(".")))
);
}
@ -108,7 +128,8 @@ class HaEntityPicker extends LitElement {
protected render(): TemplateResult | void {
const states = this._getStates(
this._hass,
this.domainFilter,
this.includeDomains,
this.excludeDomains,
this.entityFilter
);

View File

@ -1,56 +0,0 @@
import "@polymer/paper-icon-button/paper-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { EventsMixin } from "../mixins/events-mixin";
import isComponentLoaded from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
/*
* @appliesMixin EventsMixin
*/
class HaStartVoiceButton extends EventsMixin(PolymerElement) {
static get template() {
return html`
<paper-icon-button
aria-label="Start conversation"
icon="hass:microphone"
hidden$="[[!canListen]]"
on-click="handleListenClick"
></paper-icon-button>
`;
}
static get properties() {
return {
hass: {
type: Object,
value: null,
},
canListen: {
type: Boolean,
computed: "computeCanListen(hass)",
notify: true,
},
};
}
computeCanListen(hass) {
return (
"webkitSpeechRecognition" in window &&
isComponentLoaded(hass, "conversation")
);
}
handleListenClick() {
fireEvent(this, "show-dialog", {
dialogImport: () =>
import(/* webpackChunkName: "voice-command-dialog" */ "../dialogs/ha-voice-command-dialog"),
dialogTag: "ha-voice-command-dialog",
});
}
}
customElements.define("ha-start-voice-button", HaStartVoiceButton);

16
src/data/conversation.ts Normal file
View File

@ -0,0 +1,16 @@
import { HomeAssistant } from "../types";
interface ProcessResults {
card: { [key: string]: { [key: string]: string } };
speech: {
[SpeechType in "plain" | "ssml"]: { extra_data: any; speech: string }
};
}
export const processText = (
hass: HomeAssistant,
text: string,
// tslint:disable-next-line: variable-name
conversation_id: string
): Promise<ProcessResults> =>
hass.callApi("POST", "conversation/process", { text, conversation_id });

View File

@ -1,6 +1,8 @@
import { HomeAssistant } from "../types";
import { createCollection, Connection } from "home-assistant-js-websocket";
import { debounce } from "../common/util/debounce";
import { EntityRegistryEntry } from "./entity_registry";
import { computeStateName } from "../common/entity/compute_state_name";
export interface DeviceRegistryEntry {
id: string;
@ -20,6 +22,33 @@ export interface DeviceRegistryEntryMutableParams {
name_by_user?: string | null;
}
export const computeDeviceName = (
device: DeviceRegistryEntry,
hass: HomeAssistant,
entities?: EntityRegistryEntry[] | string[]
) => {
return (
device.name_by_user ||
device.name ||
(entities && fallbackDeviceName(hass, entities)) ||
hass.localize("ui.panel.config.devices.unnamed_device")
);
};
export const fallbackDeviceName = (
hass: HomeAssistant,
entities: EntityRegistryEntry[] | string[]
) => {
for (const entity of entities || []) {
const entityId = typeof entity === "string" ? entity : entity.entity_id;
const stateObj = hass.states[entityId];
if (stateObj) {
return computeStateName(stateObj);
}
}
return undefined;
};
export const updateDeviceRegistryEntry = (
hass: HomeAssistant,
deviceId: string,

View File

@ -1,5 +1,6 @@
import { HomeAssistant } from "../types";
import { Connection, getCollection } from "home-assistant-js-websocket";
import { HASSDomEvent } from "../common/dom/fire_event";
export interface LovelaceConfig {
title?: string;
@ -127,6 +128,13 @@ export interface WindowWithLovelaceProm extends Window {
llConfProm?: Promise<LovelaceConfig>;
}
export interface LongPressOptions {
export interface ActionHandlerOptions {
hasHold?: boolean;
hasDoubleClick?: boolean;
}
export interface ActionHandlerDetail {
action: string;
}
export type ActionHandlerEvent = HASSDomEvent<ActionHandlerDetail>;

91
src/data/scene.ts Normal file
View File

@ -0,0 +1,91 @@
import {
HassEntityBase,
HassEntityAttributeBase,
} from "home-assistant-js-websocket";
import { HomeAssistant, ServiceCallResponse } from "../types";
export const SCENE_IGNORED_DOMAINS = [
"sensor",
"binary_sensor",
"device_tracker",
"person",
"persistent_notification",
"configuration",
"image_processing",
"sun",
"weather",
"zone",
];
export const SCENE_SAVED_ATTRIBUTES = {
light: [
"brightness",
"color_temp",
"effect",
"rgb_color",
"xy_color",
"hs_color",
],
media_player: [
"is_volume_muted",
"volume_level",
"sound_mode",
"source",
"media_content_id",
"media_content_type",
],
climate: [
"target_temperature",
"target_temperature_high",
"target_temperature_low",
"target_humidity",
"fan_mode",
"swing_mode",
"hvac_mode",
"preset_mode",
],
vacuum: ["cleaning_mode"],
fan: ["speed", "current_direction"],
water_heather: ["temperature", "operation_mode"],
};
export interface SceneEntity extends HassEntityBase {
attributes: HassEntityAttributeBase & { id?: string };
}
export interface SceneConfig {
name: string;
entities: SceneEntities;
}
export interface SceneEntities {
[entityId: string]: string | { state: string; [key: string]: any };
}
export const activateScene = (
hass: HomeAssistant,
entityId: string
): Promise<ServiceCallResponse> =>
hass.callService("scene", "turn_on", { entity_id: entityId });
export const applyScene = (
hass: HomeAssistant,
entities: SceneEntities
): Promise<ServiceCallResponse> =>
hass.callService("scene", "apply", { entities });
export const getSceneConfig = (
hass: HomeAssistant,
sceneId: string
): Promise<SceneConfig> =>
hass.callApi<SceneConfig>("GET", `config/scene/config/${sceneId}`);
export const saveScene = (
hass: HomeAssistant,
sceneId: string,
config: SceneConfig
) => hass.callApi("POST", `config/scene/config/${sceneId}`, config);
export const deleteScene = (hass: HomeAssistant, id: string) =>
hass.callApi("DELETE", `config/scene/config/${id}`);

View File

@ -122,7 +122,13 @@ export const showConfigFlowDialog = (
<ha-markdown allowsvg .content=${description}></ha-markdown>
`
: ""}
<p>Created config for ${step.title}.</p>
<p>
${hass.localize(
"ui.panel.config.integrations.config_flow.created_config",
"name",
step.title
)}
</p>
`;
},
});

View File

@ -26,12 +26,20 @@ class StepFlowAbort extends LitElement {
protected render(): TemplateResult | void {
return html`
<h2>Aborted</h2>
<h2>
${this.hass.localize(
"ui.panel.config.integrations.config_flow.aborted"
)}
</h2>
<div class="content">
${this.flowConfig.renderAbortDescription(this.hass, this.step)}
</div>
<div class="buttons">
<mwc-button @click="${this._flowDone}">Close</mwc-button>
<mwc-button @click="${this._flowDone}"
>${this.hass.localize(
"ui.panel.config.integrations.config_flow.close"
)}</mwc-button
>
</div>
`;
}

View File

@ -63,7 +63,9 @@ class StepFlowCreateEntry extends LitElement {
${device.model} (${device.manufacturer})
</div>
<paper-dropdown-menu-light
label="Area"
label="${localize(
"ui.panel.config.integrations.config_flow.area_picker_label"
)}"
.device=${device.id}
@selected-item-changed=${this._handleAreaChanged}
>
@ -91,11 +93,19 @@ class StepFlowCreateEntry extends LitElement {
<div class="buttons">
${this.devices.length > 0
? html`
<mwc-button @click="${this._addArea}">Add Area</mwc-button>
<mwc-button @click="${this._addArea}"
>${localize(
"ui.panel.config.integrations.config_flow.add_area"
)}</mwc-button
>
`
: ""}
<mwc-button @click="${this._flowDone}">Finish</mwc-button>
<mwc-button @click="${this._flowDone}"
>${localize(
"ui.panel.config.integrations.config_flow.finish"
)}</mwc-button
>
</div>
`;
}
@ -105,7 +115,11 @@ class StepFlowCreateEntry extends LitElement {
}
private async _addArea() {
const name = prompt("Name of the new area?");
const name = prompt(
this.hass.localize(
"ui.panel.config.integrations.config_flow.name_new_area"
)
);
if (!name) {
return;
}
@ -115,7 +129,11 @@ class StepFlowCreateEntry extends LitElement {
});
this.areas = [...this.areas, area];
} catch (err) {
alert("Failed to create area.");
alert(
this.hass.localize(
"ui.panel.config.integrations.config_flow.failed_create_area"
)
);
}
}
@ -134,7 +152,13 @@ class StepFlowCreateEntry extends LitElement {
area_id: area,
});
} catch (err) {
alert(`Error saving area: ${err.message}`);
alert(
this.hass.localize(
"ui.panel.config.integrations.config_flow.error_saving_area",
"error",
"err.message"
)
);
dropdown.value = null;
}
}

View File

@ -87,14 +87,17 @@ class StepFlowForm extends LitElement {
<mwc-button
@click=${this._submitStep}
.disabled=${!allRequiredInfoFilledIn}
>
Submit
>${this.hass.localize(
"ui.panel.config.integrations.config_flow.submit"
)}
</mwc-button>
${!allRequiredInfoFilledIn
? html`
<paper-tooltip position="left">
Not all required fields are filled in.
<paper-tooltip position="left"
>${this.hass.localize(
"ui.panel.config.integrations.config_flow.not_all_required_fields"
)}
</paper-tooltip>
`
: html``}

View File

@ -36,6 +36,7 @@ class DialogConfirmation extends LitElement {
<ha-paper-dialog
with-backdrop
opened
modal
@opened-changed="${this._openedChanged}"
>
<h2>
@ -48,10 +49,14 @@ class DialogConfirmation extends LitElement {
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
<mwc-button @click="${this._dismiss}">
${this.hass.localize("ui.dialogs.confirmation.cancel")}
${this._params.cancelBtnText
? this._params.cancelBtnText
: this.hass.localize("ui.dialogs.confirmation.cancel")}
</mwc-button>
<mwc-button @click="${this._confirm}">
${this.hass.localize("ui.dialogs.confirmation.ok")}
${this._params.confirmBtnText
? this._params.confirmBtnText
: this.hass.localize("ui.dialogs.confirmation.ok")}
</mwc-button>
</div>
</ha-paper-dialog>

View File

@ -3,6 +3,8 @@ import { fireEvent } from "../../common/dom/fire_event";
export interface ConfirmationDialogParams {
title?: string;
text: string;
confirmBtnText?: string;
cancelBtnText?: string;
confirm: () => void;
}

View File

@ -24,6 +24,7 @@ import {
subscribeAreaRegistry,
AreaRegistryEntry,
} from "../../data/area_registry";
import { computeDeviceName } from "../../data/device_registry";
@customElement("dialog-device-registry-detail")
class DialogDeviceRegistryDetail extends LitElement {
@ -74,7 +75,9 @@ class DialogDeviceRegistryDetail extends LitElement {
opened
@opened-changed="${this._openedChanged}"
>
<h2>${device.name || "Unnamed device"}</h2>
<h2>
${computeDeviceName(device, this.hass)}
</h2>
<paper-dialog-scrollable>
${this._error
? html`
@ -90,7 +93,12 @@ class DialogDeviceRegistryDetail extends LitElement {
.disabled=${this._submitting}
></paper-input>
<div class="area">
<paper-dropdown-menu label="Area" class="flex">
<paper-dropdown-menu
label="${this.hass.localize(
"ui.panel.config.devices.area_picker_label"
)}"
class="flex"
>
<paper-listbox
slot="dropdown-content"
.selected="${this._computeSelectedArea()}"
@ -163,7 +171,9 @@ class DialogDeviceRegistryDetail extends LitElement {
});
this._params = undefined;
} catch (err) {
this._error = err.message || "Unknown error";
this._error =
err.message ||
this.hass.localize("ui.panel.config.devices.unknown_error");
} finally {
this._submitting = false;
}

View File

@ -37,7 +37,9 @@ class DomainTogglerDialog extends LitElement {
opened
@opened-changed=${this._openedChanged}
>
<h2>Toggle Domains</h2>
<h2>
${this.hass.localize("ui.dialogs.domain_toggler.title")}
</h2>
<div>
${domains.map(
(domain) =>

View File

@ -9,7 +9,7 @@ import "./more-info/more-info-controls";
import "./more-info/more-info-settings";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import isComponentLoaded from "../common/config/is_component_loaded";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import DialogMixin from "../mixins/dialog-mixin";

View File

@ -1,266 +0,0 @@
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-dialog-behavior/paper-dialog-shared-styles";
import "@polymer/paper-icon-button/paper-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import DialogMixin from "../mixins/dialog-mixin";
/*
* @appliesMixin DialogMixin
*/
class HaVoiceCommandDialog extends DialogMixin(PolymerElement) {
static get template() {
return html`
<style include="paper-dialog-shared-styles">
iron-icon {
margin-right: 8px;
}
.content {
width: 450px;
min-height: 80px;
font-size: 18px;
padding: 16px;
}
.messages {
max-height: 50vh;
overflow: auto;
}
.messages::after {
content: "";
clear: both;
display: block;
}
.message {
clear: both;
margin: 8px 0;
padding: 8px;
border-radius: 15px;
}
.message.user {
margin-left: 24px;
float: right;
text-align: right;
border-bottom-right-radius: 0px;
background-color: var(--light-primary-color);
color: var(--primary-text-color);
}
.message.hass {
margin-right: 24px;
float: left;
border-bottom-left-radius: 0px;
background-color: var(--primary-color);
color: var(--text-primary-color);
}
.message.error {
background-color: var(--google-red-500);
color: var(--text-primary-color);
}
.icon {
text-align: center;
}
.icon paper-icon-button {
height: 52px;
width: 52px;
}
.interimTranscript {
color: darkgrey;
}
[hidden] {
display: none;
}
:host {
border-radius: 2px;
}
@media all and (max-width: 450px) {
:host {
margin: 0;
width: 100%;
max-height: calc(100% - 64px);
position: fixed !important;
bottom: 0px;
left: 0px;
right: 0px;
overflow: scroll;
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}
.content {
width: auto;
}
.messages {
max-height: 68vh;
}
}
</style>
<div class="content">
<div class="messages" id="messages">
<template is="dom-repeat" items="[[_conversation]]" as="message">
<div class$="[[_computeMessageClasses(message)]]">
[[message.text]]
</div>
</template>
</div>
<template is="dom-if" if="[[results]]">
<div class="messages">
<div class="message user">
<span>{{results.final}}</span>
<span class="interimTranscript">[[results.interim]]</span>
</div>
</div>
</template>
<div class="icon" hidden$="[[results]]">
<paper-icon-button
icon="hass:text-to-speech"
on-click="startListening"
></paper-icon-button>
</div>
</div>
`;
}
static get properties() {
return {
hass: Object,
results: {
type: Object,
value: null,
observer: "_scrollMessagesBottom",
},
_conversation: {
type: Array,
value: function() {
return [{ who: "hass", text: "How can I help?" }];
},
observer: "_scrollMessagesBottom",
},
};
}
static get observers() {
return ["dialogOpenChanged(opened)"];
}
showDialog() {
this.opened = true;
}
initRecognition() {
/* eslint-disable new-cap */
this.recognition = new webkitSpeechRecognition();
/* eslint-enable new-cap */
this.recognition.onstart = function() {
this.results = {
final: "",
interim: "",
};
}.bind(this);
this.recognition.onerror = function() {
this.recognition.abort();
var text = this.results.final || this.results.interim;
this.results = null;
if (text === "") {
text = "<Home Assistant did not hear anything>";
}
this.push("_conversation", { who: "user", text: text, error: true });
}.bind(this);
this.recognition.onend = function() {
// Already handled by onerror
if (this.results == null) {
return;
}
var text = this.results.final || this.results.interim;
this.results = null;
this.push("_conversation", { who: "user", text: text });
this.hass.callApi("post", "conversation/process", { text: text }).then(
function(response) {
this.push("_conversation", {
who: "hass",
text: response.speech.plain.speech,
});
}.bind(this),
function() {
this.set(
["_conversation", this._conversation.length - 1, "error"],
true
);
}.bind(this)
);
}.bind(this);
this.recognition.onresult = function(event) {
var oldResults = this.results;
var finalTranscript = "";
var interimTranscript = "";
for (var ind = event.resultIndex; ind < event.results.length; ind++) {
if (event.results[ind].isFinal) {
finalTranscript += event.results[ind][0].transcript;
} else {
interimTranscript += event.results[ind][0].transcript;
}
}
this.results = {
interim: interimTranscript,
final: oldResults.final + finalTranscript,
};
}.bind(this);
}
startListening() {
if (!this.recognition) {
this.initRecognition();
}
this.results = {
interim: "",
final: "",
};
this.recognition.start();
}
_scrollMessagesBottom() {
setTimeout(() => {
this.$.messages.scrollTop = this.$.messages.scrollHeight;
if (this.$.messages.scrollTop !== 0) {
this.$.dialog.fire("iron-resize");
}
}, 10);
}
dialogOpenChanged(newVal) {
if (newVal) {
this.startListening();
} else if (!newVal && this.results) {
this.recognition.abort();
}
}
_computeMessageClasses(message) {
return "message " + message.who + (message.error ? " error" : "");
}
}
customElements.define("ha-voice-command-dialog", HaVoiceCommandDialog);

View File

@ -11,7 +11,7 @@ import "../../../components/ha-paper-dropdown-menu";
import HassMediaPlayerEntity from "../../../util/hass-media-player-model";
import { attributeClassNames } from "../../../common/entity/attribute_class_names";
import isComponentLoaded from "../../../common/config/is_component_loaded";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { EventsMixin } from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
import { computeRTLDirection } from "../../../common/util/compute_rtl";

View File

@ -13,7 +13,7 @@ import "./controls/more-info-content";
import { computeStateName } from "../../common/entity/compute_state_name";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import isComponentLoaded from "../../common/config/is_component_loaded";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const";
import { EventsMixin } from "../../mixins/events-mixin";
import { computeRTL } from "../../common/util/compute_rtl";

View File

@ -0,0 +1,455 @@
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../components/dialog/ha-paper-dialog";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import {
LitElement,
html,
property,
CSSResult,
css,
customElement,
query,
PropertyValues,
TemplateResult,
} from "lit-element";
import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
import { processText } from "../../data/conversation";
import { classMap } from "lit-html/directives/class-map";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { haStyleDialog } from "../../resources/styles";
// tslint:disable-next-line
import { PaperDialogScrollableElement } from "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import { uid } from "../../common/util/uid";
interface Message {
who: string;
text?: string;
error?: boolean;
}
interface Results {
transcript: string;
final: boolean;
}
/* tslint:disable */
// @ts-ignore
window.SpeechRecognition =
// @ts-ignore
window.SpeechRecognition || window.webkitSpeechRecognition;
// @ts-ignore
window.SpeechGrammarList =
// @ts-ignore
window.SpeechGrammarList || window.webkitSpeechGrammarList;
// @ts-ignore
window.SpeechRecognitionEvent =
// @ts-ignore
window.SpeechRecognitionEvent || window.webkitSpeechRecognitionEvent;
/* tslint:enable */
@customElement("ha-voice-command-dialog")
export class HaVoiceCommandDialog extends LitElement {
@property() public hass!: HomeAssistant;
@property() public results: Results | null = null;
@property() private _conversation: Message[] = [
{
who: "hass",
text: "",
},
];
@property() private _opened = false;
@query("#messages") private messages!: PaperDialogScrollableElement;
private recognition?: SpeechRecognition;
private _conversationId?: string;
public async showDialog(): Promise<void> {
this._opened = true;
if (SpeechRecognition) {
this._startListening();
}
}
protected render(): TemplateResult {
// CSS custom property mixins only work in render https://github.com/Polymer/lit-element/issues/633
return html`
<style>
paper-dialog-scrollable {
--paper-dialog-scrollable: {
-webkit-overflow-scrolling: auto;
max-height: 50vh !important;
}
}
paper-dialog-scrollable.can-scroll {
--paper-dialog-scrollable: {
-webkit-overflow-scrolling: touch;
max-height: 50vh !important;
}
}
@media all and (max-width: 450px), all and (max-height: 500px) {
paper-dialog-scrollable {
--paper-dialog-scrollable: {
-webkit-overflow-scrolling: auto;
max-height: calc(100vh - 175px) !important;
}
}
paper-dialog-scrollable.can-scroll {
--paper-dialog-scrollable: {
-webkit-overflow-scrolling: touch;
max-height: calc(100vh - 175px) !important;
}
}
}
</style>
<ha-paper-dialog
with-backdrop
.opened=${this._opened}
@opened-changed=${this._openedChanged}
>
<paper-dialog-scrollable id="messages">
${this._conversation.map(
(message) => html`
<div class="${this._computeMessageClasses(message)}">
${message.text}
</div>
`
)}
${this.results
? html`
<div class="message user">
<span
class=${classMap({
interimTranscript: !this.results.final,
})}
>${this.results.transcript}</span
>${!this.results.final ? "…" : ""}
</div>
`
: ""}
</paper-dialog-scrollable>
<paper-input
@keyup=${this._handleKeyUp}
label="${this.hass!.localize(
`ui.dialogs.voice_command.${
SpeechRecognition ? "label_voice" : "label"
}`
)}"
autofocus
>
${SpeechRecognition
? html`
<span suffix="" slot="suffix">
${this.results
? html`
<div class="bouncer">
<div class="double-bounce1"></div>
<div class="double-bounce2"></div>
</div>
`
: ""}
<paper-icon-button
.active=${Boolean(this.results)}
icon="hass:microphone"
@click=${this._toggleListening}
>
</paper-icon-button>
</span>
`
: ""}
</paper-input>
</ha-paper-dialog>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.updated(changedProps);
this._conversationId = uid();
this._conversation = [
{
who: "hass",
text: this.hass.localize("ui.dialogs.voice_command.how_can_i_help"),
},
];
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("_conversation") || changedProps.has("results")) {
this._scrollMessagesBottom();
}
}
private _addMessage(message: Message) {
this._conversation = [...this._conversation, message];
}
private _handleKeyUp(ev: KeyboardEvent) {
const input = ev.target as PaperInputElement;
if (ev.keyCode === 13 && input.value) {
this._processText(input.value);
input.value = "";
}
}
private _initRecognition() {
this.recognition = new SpeechRecognition();
this.recognition.interimResults = true;
this.recognition.lang = "en-US";
this.recognition!.onstart = () => {
this.results = {
final: false,
transcript: "",
};
};
this.recognition!.onerror = (event) => {
this.recognition!.abort();
if (event.error !== "aborted") {
const text =
this.results && this.results.transcript
? this.results.transcript
: `<${this.hass.localize(
"ui.dialogs.voice_command.did_not_hear"
)}>`;
this._addMessage({ who: "user", text, error: true });
}
this.results = null;
};
this.recognition!.onend = () => {
// Already handled by onerror
if (this.results == null) {
return;
}
const text = this.results.transcript;
this.results = null;
if (text) {
this._processText(text);
} else {
this._addMessage({
who: "user",
text: `<${this.hass.localize(
"ui.dialogs.voice_command.did_not_hear"
)}>`,
error: true,
});
}
};
this.recognition!.onresult = (event) => {
const result = event.results[0];
this.results = {
transcript: result[0].transcript,
final: result.isFinal,
};
};
}
private async _processText(text: string) {
if (this.recognition) {
this.recognition.abort();
}
this._addMessage({ who: "user", text });
const message: Message = {
who: "hass",
text: "…",
};
// To make sure the answer is placed at the right user text, we add it before we process it
this._addMessage(message);
try {
const response = await processText(
this.hass,
text,
this._conversationId!
);
const plain = response.speech.plain;
message.text = plain.speech;
this.requestUpdate("_conversation");
if (speechSynthesis) {
const speech = new SpeechSynthesisUtterance(
response.speech.plain.speech
);
speech.lang = "en-US";
speechSynthesis.speak(speech);
}
} catch {
message.text = this.hass.localize("ui.dialogs.voice_command.error");
message.error = true;
this.requestUpdate("_conversation");
}
}
private _toggleListening() {
if (!this.results) {
this._startListening();
} else {
this.recognition!.stop();
}
}
private _startListening() {
if (!this.recognition) {
this._initRecognition();
}
if (this.results) {
return;
}
this.results = {
transcript: "",
final: false,
};
this.recognition!.start();
}
private _scrollMessagesBottom() {
this.messages.scrollTarget.scrollTop = this.messages.scrollTarget.scrollHeight;
if (this.messages.scrollTarget.scrollTop === 0) {
fireEvent(this.messages, "iron-resize");
}
}
private _openedChanged(ev: CustomEvent) {
this._opened = ev.detail.value;
if (!this._opened && this.recognition) {
this.recognition.abort();
}
}
private _computeMessageClasses(message: Message) {
return `message ${message.who} ${message.error ? " error" : ""}`;
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
:host {
z-index: 103;
}
paper-icon-button {
color: var(--secondary-text-color);
}
paper-icon-button[active] {
color: var(--primary-color);
}
paper-input {
margin: 0 0 16px 0;
}
ha-paper-dialog {
width: 450px;
}
.message {
font-size: 18px;
clear: both;
margin: 8px 0;
padding: 8px;
border-radius: 15px;
}
.message.user {
margin-left: 24px;
float: right;
text-align: right;
border-bottom-right-radius: 0px;
background-color: var(--light-primary-color);
color: var(--primary-text-color);
}
.message.hass {
margin-right: 24px;
float: left;
border-bottom-left-radius: 0px;
background-color: var(--primary-color);
color: var(--text-primary-color);
}
.message a {
color: var(--text-primary-color);
}
.message img {
width: 100%;
border-radius: 10px;
}
.message.error {
background-color: var(--google-red-500);
color: var(--text-primary-color);
}
.interimTranscript {
color: var(--secondary-text-color);
}
.bouncer {
width: 40px;
height: 40px;
position: absolute;
top: 0;
}
.double-bounce1,
.double-bounce2 {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: var(--primary-color);
opacity: 0.2;
position: absolute;
top: 0;
left: 0;
-webkit-animation: sk-bounce 2s infinite ease-in-out;
animation: sk-bounce 2s infinite ease-in-out;
}
.double-bounce2 {
-webkit-animation-delay: -1s;
animation-delay: -1s;
}
@-webkit-keyframes sk-bounce {
0%,
100% {
-webkit-transform: scale(0);
}
50% {
-webkit-transform: scale(1);
}
}
@keyframes sk-bounce {
0%,
100% {
transform: scale(0);
-webkit-transform: scale(0);
}
50% {
transform: scale(1);
-webkit-transform: scale(1);
}
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.message {
font-size: 16px;
}
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-voice-command-dialog": HaVoiceCommandDialog;
}
}

View File

@ -0,0 +1,12 @@
import { fireEvent } from "../../common/dom/fire_event";
const loadVoiceCommandDialog = () =>
import(/* webpackChunkName: "ha-voice-command-dialog" */ "./ha-voice-command-dialog");
export const showVoiceCommandDialog = (element: HTMLElement): void => {
fireEvent(element, "show-dialog", {
dialogTag: "ha-voice-command-dialog",
dialogImport: loadVoiceCommandDialog,
dialogParams: {},
});
};

View File

@ -180,6 +180,7 @@ export const provideHass = (
dockedSidebar: "auto",
vibrate: true,
moreInfoEntityId: null as any,
// @ts-ignore
async callService(domain, service, data) {
if (data && "entity_id" in data) {
await Promise.all(

View File

@ -5,14 +5,13 @@ export interface ProvideHassElement {
provideHass(element: HTMLElement);
}
/* tslint:disable */
/* tslint:disable-next-line:variable-name */
export const ProvideHassLitMixin = <T extends Constructor<UpdatingElement>>(
superClass: T
) =>
// @ts-ignore
class extends superClass {
protected hass!: HomeAssistant;
/* tslint:disable-next-line:variable-name */
private __provideHass: HTMLElement[] = [];
public provideHass(el) {

View File

@ -1,4 +1,4 @@
import { LitElement, PropertyValues, property } from "lit-element";
import { PropertyValues, property, UpdatingElement } from "lit-element";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { HomeAssistant, Constructor } from "../types";
@ -6,14 +6,14 @@ export interface HassSubscribeElement {
hassSubscribe(): UnsubscribeFunc[];
}
/* tslint:disable-next-line */
export const SubscribeMixin = <T extends Constructor<LitElement>>(
/* tslint:disable-next-line:variable-name */
export const SubscribeMixin = <T extends Constructor<UpdatingElement>>(
superClass: T
) => {
class SubscribeClass extends superClass {
@property() public hass?: HomeAssistant;
/* tslint:disable-next-line */
/* tslint:disable-next-line:variable-name */
private __unsubs?: UnsubscribeFunc[];
public connectedCallback() {

View File

@ -82,6 +82,9 @@ export class HaAutomationEditor extends LitElement {
? ""
: html`
<paper-icon-button
title="${this.hass.localize(
"ui.panel.config.automation.picker.delete_automation"
)}"
icon="hass:delete"
@click=${this._delete}
></paper-icon-button>
@ -218,7 +221,11 @@ export class HaAutomationEditor extends LitElement {
}
private async _delete() {
if (!confirm("Are you sure you want to delete this automation?")) {
if (
!confirm(
this.hass.localize("ui.panel.config.automation.picker.delete_confirm")
)
) {
return;
}
await deleteAutomation(this.hass, this.automation.attributes.id!);

View File

@ -85,16 +85,16 @@ class HaAutomationPicker extends LitElement {
<paper-item-body two-line>
<div>${computeStateName(automation)}</div>
<div secondary>
Last triggered: ${
automation.attributes.last_triggered
? format_date_time(
new Date(
automation.attributes.last_triggered
),
this.hass.language
)
: "never"
}
${this.hass.localize(
"ui.card.automation.last_triggered"
)}: ${
automation.attributes.last_triggered
? format_date_time(
new Date(automation.attributes.last_triggered),
this.hass.language
)
: this.hass.localize("ui.components.relative_time.never")
}
</div>
</paper-item-body>
<div class='actions'>
@ -102,6 +102,9 @@ class HaAutomationPicker extends LitElement {
.automation=${automation}
@click=${this._showInfo}
icon="hass:information-outline"
title="${this.hass.localize(
"ui.panel.config.automation.picker.show_info_automation"
)}"
></paper-icon-button>
<a
href=${ifDefined(
@ -113,6 +116,9 @@ class HaAutomationPicker extends LitElement {
)}
>
<paper-icon-button
title="${this.hass.localize(
"ui.panel.config.automation.picker.edit_automation"
)}"
icon="hass:pencil"
.disabled=${!automation.attributes.id}
></paper-icon-button>
@ -120,8 +126,9 @@ class HaAutomationPicker extends LitElement {
!automation.attributes.id
? html`
<paper-tooltip position="left">
Only automations defined in
automations.yaml are editable.
${this.hass.localize(
"ui.panel.config.automation.picker.only_editable"
)}
</paper-tooltip>
`
: ""

View File

@ -9,7 +9,7 @@ import "../../../resources/ha-style";
import "../ha-config-section";
import isComponentLoaded from "../../../common/config/is_component_loaded";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import LocalizeMixin from "../../../mixins/localize-mixin";
import "./ha-config-name-form";

View File

@ -3,13 +3,14 @@ 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 LocalizeMixin from "../../../mixins/localize-mixin";
import hassAttributeUtil from "../../../util/hass-attributes-util";
import "./ha-form-customize-attributes";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
class HaFormCustomize extends PolymerElement {
class HaFormCustomize extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex ha-style ha-form-style">
@ -26,19 +27,18 @@ class HaFormCustomize extends PolymerElement {
if="[[computeShowWarning(localConfig, globalConfig)]]"
>
<div class="warning">
It seems that your configuration.yaml doesn't properly
[[localize('ui.panel.config.customize.warning.include_sentence')]]
<a
href="https://www.home-assistant.io/docs/configuration/customizing-devices/#customization-using-the-ui"
target="_blank"
>include customize.yaml</a
>[[localize('ui.panel.config.customize.warning.include_link')]]</a
>.<br />
Changes made here are written in it, but will not be applied after a
configuration reload unless the include is in place.
[[localize('ui.panel.config.customize.warning.not_applied')]]
</div>
</template>
<template is="dom-if" if="[[hasLocalAttributes]]">
<h4 class="attributes-text">
The following attributes are already set in customize.yaml<br />
[[localize('ui.panel.config.customize.attributes_customize')]]<br />
</h4>
<ha-form-customize-attributes
attributes="{{localAttributes}}"
@ -46,9 +46,8 @@ class HaFormCustomize extends PolymerElement {
</template>
<template is="dom-if" if="[[hasGlobalAttributes]]">
<h4 class="attributes-text">
The following attributes are customized from outside of
customize.yaml<br />
Possibly via a domain, a glob or a different include.
[[localize('ui.panel.config.customize.attributes_outside')]]<br />
[[localize('ui.panel.config.customize.different_include')]]
</h4>
<ha-form-customize-attributes
attributes="{{globalAttributes}}"
@ -56,8 +55,8 @@ class HaFormCustomize extends PolymerElement {
</template>
<template is="dom-if" if="[[hasExistingAttributes]]">
<h4 class="attributes-text">
The following attributes of the entity are set programatically.<br />
You can override them if you like.
[[localize('ui.panel.config.customize.attributes_set')]]<br />
[[localize('ui.panel.config.customize.attributes_override')]]
</h4>
<ha-form-customize-attributes
attributes="{{existingAttributes}}"
@ -65,7 +64,7 @@ class HaFormCustomize extends PolymerElement {
</template>
<template is="dom-if" if="[[hasNewAttributes]]">
<h4 class="attributes-text">
The following attributes weren't set. Set them if you like.
[[localize('ui.panel.config.customize.attributes_not_set')]]
</h4>
<ha-form-customize-attributes
attributes="{{newAttributes}}"
@ -73,7 +72,7 @@ class HaFormCustomize extends PolymerElement {
</template>
<div class="form-group">
<paper-dropdown-menu
label="Pick an attribute to override"
label="[[localize('ui.panel.config.customize.pick_attribute')]]"
class="flex"
dynamic-align=""
>

View File

@ -14,7 +14,7 @@ import "../../../components/ha-icon-next";
import "../ha-config-section";
import "./ha-config-navigation";
import isComponentLoaded from "../../../common/config/is_component_loaded";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import LocalizeMixin from "../../../mixins/localize-mixin";
import NavigateMixin from "../../../mixins/navigate-mixin";
@ -124,7 +124,9 @@ class HaConfigDashboard extends NavigateMixin(LocalizeMixin(PolymerElement)) {
<template is='dom-if' if='[[!showAdvanced]]'>
<div class='promo-advanced'>
Missing config options? Enable advanced mode on <a href="/profile">your profile page.</a>
[[localize('ui.panel.profile.advanced_mode.hint_enable')]] <a
href="/profile"
>[[localize('ui.panel.profile.advanced_mode.link_profile_page')]]</a>.
</div>
</template>
</ha-config-section>

View File

@ -2,7 +2,7 @@ import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-item/paper-item";
import isComponentLoaded from "../../../common/config/is_component_loaded";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-card";
import "../../../components/ha-icon-next";
@ -29,6 +29,7 @@ const PAGES: Array<{
{ page: "area_registry", core: true },
{ page: "automation" },
{ page: "script" },
{ page: "scene" },
{ page: "zha" },
{ page: "zwave" },
{ page: "customize", core: true, advanced: true },

View File

@ -1,6 +1,9 @@
import "../../../../components/ha-card";
import { DeviceRegistryEntry } from "../../../../data/device_registry";
import {
DeviceRegistryEntry,
computeDeviceName,
} from "../../../../data/device_registry";
import { loadDeviceRegistryDetailDialog } from "../../../../dialogs/device-registry-detail/show-dialog-device-registry-detail";
import {
LitElement,
@ -93,14 +96,10 @@ export class HaDeviceCard extends LitElement {
return areas.find((area) => area.area_id === device.area_id).name;
}
private _deviceName(device) {
return device.name_by_user || device.name;
}
private _computeDeviceName(devices, deviceId) {
const device = devices.find((dev) => dev.id === deviceId);
return device
? this._deviceName(device)
? computeDeviceName(device, this.hass)
: `(${this.hass.localize(
"ui.panel.config.integrations.config_entry.device_unavailable"
)})`;

View File

@ -122,7 +122,11 @@ export class HaConfigDevicePage extends LitElement {
if (!device) {
return html`
<hass-error-screen error="Device not found."></hass-error-screen>
<hass-error-screen
error="${this.hass.localize(
"ui.panel.config.devices.device_not_found"
)}"
></hass-error-screen>
`;
}
@ -136,9 +140,11 @@ export class HaConfigDevicePage extends LitElement {
@click=${this._showSettings}
></paper-icon-button>
<ha-config-section .isWide=${!this.narrow}>
<span slot="header">Device info</span>
<span slot="header"
>${this.hass.localize("ui.panel.config.devices.info")}</span
>
<span slot="introduction">
Here are all the details of your device.
${this.hass.localize("ui.panel.config.devices.details")}
</span>
<ha-device-card
.hass=${this.hass}
@ -149,7 +155,9 @@ export class HaConfigDevicePage extends LitElement {
${entities.length
? html`
<div class="header">Entities</div>
<div class="header">
${this.hass.localize("ui.panel.config.devices.entities")}
</div>
<ha-device-entities-card
.hass=${this.hass}
.entities=${entities}
@ -161,7 +169,9 @@ export class HaConfigDevicePage extends LitElement {
this._conditions.length ||
this._actions.length
? html`
<div class="header">Automations</div>
<div class="header">
${this.hass.localize("ui.panel.config.devices.automations")}
</div>
${this._triggers.length
? html`
<ha-device-triggers-card
@ -222,7 +232,9 @@ export class HaConfigDevicePage extends LitElement {
const renameEntityid =
this.showAdvanced &&
confirm(
"Do you also want to rename the entity id's of your entities?"
this.hass.localize(
"ui.panel.config.devices.confirm_rename_entity_ids"
)
);
const updateProms = entities.map((entity) => {

View File

@ -18,13 +18,15 @@ import {
DataTableRowData,
} from "../../../components/data-table/ha-data-table";
// tslint:disable-next-line
import { DeviceRegistryEntry } from "../../../data/device_registry";
import {
DeviceRegistryEntry,
computeDeviceName,
} from "../../../data/device_registry";
import { EntityRegistryEntry } from "../../../data/entity_registry";
import { ConfigEntry } from "../../../data/config_entries";
import { AreaRegistryEntry } from "../../../data/area_registry";
import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize";
import { computeStateName } from "../../../common/entity/compute_state_name";
export interface DeviceRowData extends DeviceRegistryEntry {
device?: DeviceRowData;
@ -99,11 +101,11 @@ export class HaDevicesDataTable extends LitElement {
outputDevices = outputDevices.map((device) => {
return {
...device,
name:
device.name_by_user ||
device.name ||
this._fallbackDeviceName(device.id, deviceEntityLookup) ||
"No name",
name: computeDeviceName(
device,
this.hass,
deviceEntityLookup[device.id]
),
model: device.model || "<unknown>",
manufacturer: device.manufacturer || "<unknown>",
area: device.area_id ? areaLookup[device.area_id].name : "No area",
@ -159,33 +161,45 @@ export class HaDevicesDataTable extends LitElement {
}
: {
name: {
title: "Device",
title: this.hass.localize(
"ui.panel.config.devices.data_table.device"
),
sortable: true,
filterable: true,
direction: "asc",
},
manufacturer: {
title: "Manufacturer",
title: this.hass.localize(
"ui.panel.config.devices.data_table.manufacturer"
),
sortable: true,
filterable: true,
},
model: {
title: "Model",
title: this.hass.localize(
"ui.panel.config.devices.data_table.model"
),
sortable: true,
filterable: true,
},
area: {
title: "Area",
title: this.hass.localize(
"ui.panel.config.devices.data_table.area"
),
sortable: true,
filterable: true,
},
integration: {
title: "Integration",
title: this.hass.localize(
"ui.panel.config.devices.data_table.integration"
),
sortable: true,
filterable: true,
},
battery_entity: {
title: "Battery",
title: this.hass.localize(
"ui.panel.config.devices.data_table.battery"
),
sortable: true,
type: "numeric",
template: (batteryEntity: string) => {
@ -238,20 +252,6 @@ export class HaDevicesDataTable extends LitElement {
return batteryEntity ? batteryEntity.entity_id : undefined;
}
private _fallbackDeviceName(
deviceId: string,
deviceEntityLookup: DeviceEntityLookup
): string | undefined {
for (const entity of deviceEntityLookup[deviceId] || []) {
const stateObj = this.hass.states[entity.entity_id];
if (stateObj) {
return computeStateName(stateObj);
}
}
return undefined;
}
private _handleRowClicked(ev: CustomEvent) {
const deviceId = (ev.detail as RowClickedEvent).id;
navigate(this, `/config/devices/device/${deviceId}`);

View File

@ -130,7 +130,9 @@ class DialogEntityRegistryDetail extends LitElement {
${this.hass.localize(
"ui.panel.config.entity_registry.editor.enabled_description"
)}
<br />Note: this might not work yet with all integrations.
<br />${this.hass.localize(
"ui.panel.config.entity_registry.editor.note"
)}
</div>
</div>
</ha-switch>

View File

@ -1,6 +1,6 @@
import { property, PropertyValues, customElement } from "lit-element";
import "../../layouts/hass-loading-screen";
import isComponentLoaded from "../../common/config/is_component_loaded";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { HomeAssistant } from "../../types";
import { CloudStatus, fetchCloudStatus } from "../../data/cloud";
import { listenMediaQuery } from "../../common/dom/media_query";
@ -88,6 +88,11 @@ class HaPanelConfig extends HassRouterPage {
load: () =>
import(/* webpackChunkName: "panel-config-script" */ "./script/ha-config-script"),
},
scene: {
tag: "ha-config-scene",
load: () =>
import(/* webpackChunkName: "panel-config-scene" */ "./scene/ha-config-scene"),
},
users: {
tag: "ha-config-users",
load: () =>

View File

@ -63,7 +63,11 @@ class HaConfigEntryPage extends LitElement {
if (!configEntry) {
return html`
<hass-error-screen error="Integration not found."></hass-error-screen>
<hass-error-screen
error="${this.hass.localize(
"ui.panel.config.integrations.integration_not_found"
)}"
></hass-error-screen>
`;
}

View File

@ -52,7 +52,7 @@ export default class ZoneCondition extends Component<any> {
onChange={this.zonePicked}
hass={hass}
allowCustomEntity
domainFilter="zone"
includeDomains={["zone"]}
/>
</div>
);

View File

@ -40,7 +40,7 @@ export default class ScriptEditor extends Component<{
<ha-config-section is-wide={isWide}>
<span slot="header">{alias}</span>
<span slot="introduction">
Use scripts to execute a sequence of actions.
{localize("ui.panel.config.script.editor.introduction")}
</span>
<ha-card>
<div class="card-content">
@ -55,12 +55,16 @@ export default class ScriptEditor extends Component<{
</ha-config-section>
<ha-config-section is-wide={isWide}>
<span slot="header">Sequence</span>
<span slot="header">
{localize("ui.panel.config.script.editor.sequence")}
</span>
<span slot="introduction">
The sequence of actions of this script.
{localize("ui.panel.config.script.editor.sequence_sentence")}
<p>
<a href="https://home-assistant.io/docs/scripts/" target="_blank">
Learn more about available actions.
{localize(
"ui.panel.config.script.editor.link_available_actions"
)}
</a>
</p>
</span>

View File

@ -24,7 +24,7 @@ export default class SceneAction extends Component<any> {
value={scene}
onChange={this.sceneChanged}
hass={hass}
domainFilter="scene"
includeDomains={["scene"]}
allowCustomEntity
/>
</div>

View File

@ -51,7 +51,7 @@ export default class GeolocationTrigger extends Component<any> {
onChange={this.zonePicked}
hass={hass}
allowCustomEntity
domainFilter="zone"
includeDomains={["zone"]}
/>
<label id="eventlabel">
{localize(

View File

@ -42,7 +42,7 @@ export default class ZoneTrigger extends Component<any> {
onChange={this.zonePicked}
hass={hass}
allowCustomEntity
domainFilter="zone"
includeDomains={["zone"]}
/>
<label id="eventlabel">
{localize(

View File

@ -104,7 +104,7 @@ class DialogPersonDetail extends LitElement {
<ha-entities-picker
.hass=${this.hass}
.value=${this._deviceTrackers}
domain-filter="device_tracker"
include-domains='["device_tracker"]'
.pickedEntityLabel=${this.hass.localize(
"ui.panel.config.person.detail.device_tracker_picked"
)}
@ -222,7 +222,7 @@ class DialogPersonDetail extends LitElement {
}
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
if (!(ev.detail as any).value) {
if (ev.detail.value) {
this._params = undefined;
}
}

View File

@ -0,0 +1,81 @@
import "@polymer/app-route/app-route";
import "./ha-scene-editor";
import "./ha-scene-dashboard";
import {
HassRouterPage,
RouterOptions,
} from "../../../layouts/hass-router-page";
import { property, customElement, PropertyValues } from "lit-element";
import { HomeAssistant } from "../../../types";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { compare } from "../../../common/string/compare";
import { SceneEntity } from "../../../data/scene";
import memoizeOne from "memoize-one";
import { HassEntities } from "home-assistant-js-websocket";
@customElement("ha-config-scene")
class HaConfigScene extends HassRouterPage {
@property() public hass!: HomeAssistant;
@property() public narrow!: boolean;
@property() public showAdvanced!: boolean;
@property() public scenes: SceneEntity[] = [];
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
routes: {
dashboard: {
tag: "ha-scene-dashboard",
cache: true,
},
edit: {
tag: "ha-scene-editor",
},
},
};
private _computeScenes = memoizeOne((states: HassEntities) => {
const scenes: SceneEntity[] = [];
Object.keys(states).forEach((entityId) => {
if (computeDomain(entityId) === "scene") {
scenes.push(states[entityId] as SceneEntity);
}
});
return scenes.sort((a, b) => {
return compare(computeStateName(a), computeStateName(b));
});
});
protected updatePageEl(pageEl, changedProps: PropertyValues) {
pageEl.hass = this.hass;
pageEl.narrow = this.narrow;
pageEl.showAdvanced = this.showAdvanced;
if (this.hass) {
pageEl.scenes = this._computeScenes(this.hass.states);
}
if (
(!changedProps || changedProps.has("route")) &&
this._currentPage === "edit"
) {
const sceneId = this.routeTail.path.substr(1);
pageEl.creatingNew = sceneId === "new" ? true : false;
pageEl.scene =
sceneId === "new"
? undefined
: pageEl.scenes.find(
(entity: SceneEntity) => entity.attributes.id === sceneId
);
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-scene": HaConfigScene;
}
}

View File

@ -0,0 +1,213 @@
import {
LitElement,
TemplateResult,
html,
CSSResultArray,
css,
property,
customElement,
} from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-tooltip/paper-tooltip";
import "../../../layouts/hass-subpage";
import "../../../components/ha-card";
import "../../../components/ha-fab";
import "../ha-config-section";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { computeRTL } from "../../../common/util/compute_rtl";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { SceneEntity, activateScene } from "../../../data/scene";
import { showToast } from "../../../util/toast";
import { ifDefined } from "lit-html/directives/if-defined";
import { forwardHaptic } from "../../../data/haptics";
@customElement("ha-scene-dashboard")
class HaSceneDashboard extends LitElement {
@property() public hass!: HomeAssistant;
@property() public narrow!: boolean;
@property() public scenes!: SceneEntity[];
protected render(): TemplateResult | void {
return html`
<hass-subpage
.header=${this.hass.localize("ui.panel.config.scene.caption")}
>
<ha-config-section .isWide=${!this.narrow}>
<div slot="header">
${this.hass.localize("ui.panel.config.scene.picker.header")}
</div>
<div slot="introduction">
${this.hass.localize("ui.panel.config.scene.picker.introduction")}
<p>
<a
href="https://home-assistant.io/docs/scene/editor/"
target="_blank"
>
${this.hass.localize("ui.panel.config.scene.picker.learn_more")}
</a>
</p>
</div>
<ha-card
.heading=${this.hass.localize(
"ui.panel.config.scene.picker.pick_scene"
)}
>
${this.scenes.length === 0
? html`
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.scene.picker.no_scenes"
)}
</p>
</div>
`
: this.scenes.map(
(scene) => html`
<div class='scene'>
<paper-icon-button
.scene=${scene}
icon="hass:play"
title="${this.hass.localize(
"ui.panel.config.scene.picker.activate_scene"
)}"
@click=${this._activateScene}
></paper-icon-button>
<paper-item-body two-line>
<div>${computeStateName(scene)}</div>
</paper-item-body>
<a
href=${ifDefined(
scene.attributes.id
? `/config/scene/edit/${scene.attributes.id}`
: undefined
)}
>
<paper-icon-button
title="${this.hass.localize(
"ui.panel.config.scene.picker.edit_scene"
)}"
icon="hass:pencil"
.disabled=${!scene.attributes.id}
></paper-icon-button>
${
!scene.attributes.id
? html`
<paper-tooltip position="left">
${this.hass.localize(
"ui.panel.config.scene.picker.only_editable"
)}
</paper-tooltip>
`
: ""
}
</a>
</div>
</a>
`
)}
</ha-card>
</ha-config-section>
<a href="/config/scene/edit/new">
<ha-fab
slot="fab"
?is-wide=${!this.narrow}
icon="hass:plus"
title=${this.hass.localize(
"ui.panel.config.scene.picker.add_scene"
)}
?rtl=${computeRTL(this.hass)}
></ha-fab>
</a>
</hass-subpage>
`;
}
private async _activateScene(ev) {
const scene = ev.target.scene as SceneEntity;
await activateScene(this.hass, scene.entity_id);
showToast(this, {
message: this.hass.localize(
"ui.panel.config.scene.activated",
"name",
computeStateName(scene)
),
});
forwardHaptic("light");
}
static get styles(): CSSResultArray {
return [
haStyle,
css`
:host {
display: block;
}
hass-subpage {
min-height: 100vh;
}
ha-card {
margin-bottom: 56px;
}
.scene {
display: flex;
flex-direction: horizontal;
align-items: center;
padding: 0 8px 0 16px;
}
.scene a[href] {
color: var(--primary-text-color);
}
ha-entity-toggle {
margin-right: 16px;
}
ha-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
ha-fab[is-wide] {
bottom: 24px;
right: 24px;
}
ha-fab[rtl] {
right: auto;
left: 16px;
}
ha-fab[rtl][is-wide] {
bottom: 24px;
right: auto;
left: 24px;
}
a {
color: var(--primary-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-scene-dashboard": HaSceneDashboard;
}
}

View File

@ -0,0 +1,738 @@
import {
LitElement,
TemplateResult,
html,
CSSResult,
css,
PropertyValues,
property,
customElement,
} from "lit-element";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import { classMap } from "lit-html/directives/class-map";
import "../../../components/ha-fab";
import "../../../components/device/ha-device-picker";
import "../../../components/entity/ha-entities-picker";
import "../../../components/ha-paper-icon-button-arrow-prev";
import "../../../layouts/ha-app-layout";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { navigate } from "../../../common/navigate";
import { computeRTL } from "../../../common/util/compute_rtl";
import {
SceneEntity,
SceneConfig,
getSceneConfig,
deleteScene,
saveScene,
SCENE_IGNORED_DOMAINS,
SceneEntities,
SCENE_SAVED_ATTRIBUTES,
applyScene,
activateScene,
} from "../../../data/scene";
import { fireEvent } from "../../../common/dom/fire_event";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
computeDeviceName,
} from "../../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../data/entity_registry";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import memoizeOne from "memoize-one";
import { computeDomain } from "../../../common/entity/compute_domain";
import { HassEvent } from "home-assistant-js-websocket";
import { showConfirmationDialog } from "../../../dialogs/confirmation/show-dialog-confirmation";
interface DeviceEntities {
id: string;
name: string;
entities: string[];
}
interface DeviceEntitiesLookup {
[deviceId: string]: string[];
}
@customElement("ha-scene-editor")
export class HaSceneEditor extends SubscribeMixin(LitElement) {
@property() public hass!: HomeAssistant;
@property() public narrow?: boolean;
@property() public scene?: SceneEntity;
@property() public creatingNew?: boolean;
@property() public showAdvanced!: boolean;
@property() private _dirty?: boolean;
@property() private _errors?: string;
@property() private _config!: SceneConfig;
@property() private _entities: string[] = [];
@property() private _devices: string[] = [];
@property() private _deviceRegistryEntries: DeviceRegistryEntry[] = [];
@property() private _entityRegistryEntries: EntityRegistryEntry[] = [];
private _storedStates: SceneEntities = {};
private _unsubscribeEvents?: () => void;
private _deviceEntityLookup: DeviceEntitiesLookup = {};
private _activateContextId?: string;
private _getEntitiesDevices = memoizeOne(
(
entities: string[],
devices: string[],
deviceEntityLookup: DeviceEntitiesLookup,
deviceRegs: DeviceRegistryEntry[]
) => {
const outputDevices: DeviceEntities[] = [];
if (devices.length) {
const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {};
for (const device of deviceRegs) {
deviceLookup[device.id] = device;
}
devices.forEach((deviceId) => {
const device = deviceLookup[deviceId];
const deviceEntities: string[] = deviceEntityLookup[deviceId] || [];
outputDevices.push({
name: computeDeviceName(
device,
this.hass,
this._deviceEntityLookup[device.id]
),
id: device.id,
entities: deviceEntities,
});
});
}
const outputEntities: string[] = [];
entities.forEach((entity) => {
if (!outputDevices.find((device) => device.entities.includes(entity))) {
outputEntities.push(entity);
}
});
return { devices: outputDevices, entities: outputEntities };
}
);
public disconnectedCallback() {
super.disconnectedCallback();
if (this._unsubscribeEvents) {
this._unsubscribeEvents();
this._unsubscribeEvents = undefined;
}
}
public hassSubscribe() {
return [
subscribeEntityRegistry(this.hass.connection, (entries) => {
this._entityRegistryEntries = entries;
}),
subscribeDeviceRegistry(this.hass.connection, (entries) => {
this._deviceRegistryEntries = entries;
}),
];
}
protected render(): TemplateResult | void {
if (!this.hass) {
return;
}
const { devices, entities } = this._getEntitiesDevices(
this._entities,
this._devices,
this._deviceEntityLookup,
this._deviceRegistryEntries
);
return html`
<ha-app-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<ha-paper-icon-button-arrow-prev
@click=${this._backTapped}
></ha-paper-icon-button-arrow-prev>
<div main-title>
${this.scene
? computeStateName(this.scene)
: this.hass.localize(
"ui.panel.config.scene.editor.default_name"
)}
</div>
${this.creatingNew
? ""
: html`
<paper-icon-button
title="${this.hass.localize(
"ui.panel.config.scene.picker.delete_scene"
)}"
icon="hass:delete"
@click=${this._deleteTapped}
></paper-icon-button>
`}
</app-toolbar>
</app-header>
<div class="content">
${this._errors
? html`
<div class="errors">${this._errors}</div>
`
: ""}
<div
id="root"
class="${classMap({
rtl: computeRTL(this.hass),
})}"
>
<ha-config-section .isWide=${!this.narrow}>
<div slot="header">
${this.scene
? computeStateName(this.scene)
: this.hass.localize(
"ui.panel.config.scene.editor.default_name"
)}
</div>
<div slot="introduction">
${this.hass.localize(
"ui.panel.config.scene.editor.introduction"
)}
</div>
<ha-card>
<div class="card-content">
<paper-input
.value=${this.scene ? computeStateName(this.scene) : ""}
@value-changed=${this._nameChanged}
label=${this.hass.localize(
"ui.panel.config.scene.editor.name"
)}
></paper-input>
</div>
</ha-card>
</ha-config-section>
<ha-config-section .isWide=${!this.narrow}>
<div slot="header">
${this.hass.localize(
"ui.panel.config.scene.editor.devices.header"
)}
</div>
<div slot="introduction">
${this.hass.localize(
"ui.panel.config.scene.editor.devices.introduction"
)}
</div>
${devices.map(
(device) =>
html`
<ha-card>
<div class="card-header">
${device.name}
<paper-icon-button
icon="hass:delete"
title="${this.hass.localize(
"ui.panel.config.scene.editor.devices.delete"
)}"
.device=${device.id}
@click=${this._deleteDevice}
></paper-icon-button>
</div>
${device.entities.map((entity) => {
const stateObj = this.hass.states[entity];
if (!stateObj) {
return html``;
}
return html`
<paper-icon-item
.entity=${stateObj.entity_id}
@click=${this._showMoreInfo}
class="device-entity"
>
<state-badge
.stateObj=${stateObj}
slot="item-icon"
></state-badge>
<paper-item-body>
${computeStateName(stateObj)}
</paper-item-body>
</paper-icon-item>
`;
})}
</ha-card>
`
)}
<ha-card
.header=${this.hass.localize(
"ui.panel.config.scene.editor.devices.add"
)}
>
<div class="card-content">
<ha-device-picker
@value-changed=${this._devicePicked}
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.scene.editor.devices.add"
)}
/>
</div>
</ha-card>
</ha-config-section>
${this.showAdvanced
? html`
<ha-config-section .isWide=${!this.narrow}>
<div slot="header">
${this.hass.localize(
"ui.panel.config.scene.editor.entities.header"
)}
</div>
<div slot="introduction">
${this.hass.localize(
"ui.panel.config.scene.editor.entities.introduction"
)}
</div>
${entities.length
? html`
<ha-card
class="entities"
.header=${this.hass.localize(
"ui.panel.config.scene.editor.entities.without_device"
)}
>
${entities.map((entity) => {
const stateObj = this.hass.states[entity];
if (!stateObj) {
return html``;
}
return html`
<paper-icon-item
.entity=${stateObj.entity_id}
@click=${this._showMoreInfo}
class="device-entity"
>
<state-badge
.stateObj=${stateObj}
slot="item-icon"
></state-badge>
<paper-item-body>
${computeStateName(stateObj)}
</paper-item-body>
<paper-icon-button
icon="hass:delete"
.entity=${entity}
.title="${this.hass.localize(
"ui.panel.config.scene.editor.entities.delete"
)}"
@click=${this._deleteEntity}
></paper-icon-button>
</paper-icon-item>
`;
})}
</ha-card>
`
: ""}
<ha-card
header=${this.hass.localize(
"ui.panel.config.scene.editor.entities.add"
)}
>
<div class="card-content">
${this.hass.localize(
"ui.panel.config.scene.editor.entities.device_entities"
)}
<ha-entity-picker
@value-changed=${this._entityPicked}
.excludeDomains=${SCENE_IGNORED_DOMAINS}
.hass=${this.hass}
label=${this.hass.localize(
"ui.panel.config.scene.editor.entities.add"
)}
/>
</div>
</ha-card>
</ha-config-section>
`
: ""}
</div>
</div>
<ha-fab
slot="fab"
?is-wide="${!this.narrow}"
?dirty="${this._dirty}"
icon="hass:content-save"
.title="${this.hass.localize("ui.panel.config.scene.editor.save")}"
@click=${this._saveScene}
class="${classMap({
rtl: computeRTL(this.hass),
})}"
></ha-fab>
</ha-app-layout>
`;
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
const oldscene = changedProps.get("scene") as SceneEntity;
if (
changedProps.has("scene") &&
this.scene &&
this.hass &&
// Only refresh config if we picked a new scene. If same ID, don't fetch it.
(!oldscene || oldscene.attributes.id !== this.scene.attributes.id)
) {
this._loadConfig();
}
if (changedProps.has("creatingNew") && this.creatingNew && this.hass) {
this._dirty = false;
this._config = {
name: this.hass.localize("ui.panel.config.scene.editor.default_name"),
entities: {},
};
}
if (changedProps.has("_entityRegistryEntries")) {
for (const entity of this._entityRegistryEntries) {
if (
!entity.device_id ||
SCENE_IGNORED_DOMAINS.includes(computeDomain(entity.entity_id))
) {
continue;
}
if (!(entity.device_id in this._deviceEntityLookup)) {
this._deviceEntityLookup[entity.device_id] = [];
}
if (
!this._deviceEntityLookup[entity.device_id].includes(entity.entity_id)
) {
this._deviceEntityLookup[entity.device_id].push(entity.entity_id);
}
}
}
}
private _showMoreInfo(ev: Event) {
const entityId = (ev.currentTarget as any).entity;
fireEvent(this, "hass-more-info", { entityId });
}
private async _loadConfig() {
let config: SceneConfig;
try {
config = await getSceneConfig(this.hass, this.scene!.attributes.id!);
} catch (err) {
alert(
err.status_code === 404
? this.hass.localize(
"ui.panel.config.scene.editor.load_error_not_editable"
)
: this.hass.localize(
"ui.panel.config.scene.editor.load_error_unknown",
"err_no",
err.status_code
)
);
history.back();
return;
}
if (!config.entities) {
config.entities = {};
}
this._entities = Object.keys(config.entities);
this._entities.forEach((entity) => {
this._storeState(entity);
});
const filteredEntityReg = this._entityRegistryEntries.filter((entityReg) =>
this._entities.includes(entityReg.entity_id)
);
for (const entityReg of filteredEntityReg) {
if (!entityReg.device_id) {
continue;
}
if (!this._devices.includes(entityReg.device_id)) {
this._devices = [...this._devices, entityReg.device_id];
}
}
const { context } = await activateScene(this.hass, this.scene!.entity_id);
this._activateContextId = context.id;
this._unsubscribeEvents = await this.hass!.connection.subscribeEvents<
HassEvent
>((event) => this._stateChanged(event), "state_changed");
this._dirty = false;
this._config = config;
}
private _entityPicked(ev: CustomEvent) {
const entityId = ev.detail.value;
(ev.target as any).value = "";
if (this._entities.includes(entityId)) {
return;
}
this._entities = [...this._entities, entityId];
this._storeState(entityId);
this._dirty = true;
}
private _deleteEntity(ev: Event) {
ev.stopPropagation();
const deleteEntityId = (ev.target as any).entityId;
this._entities = this._entities.filter(
(entityId) => entityId !== deleteEntityId
);
this._dirty = true;
}
private _devicePicked(ev: CustomEvent) {
const device = ev.detail.value;
(ev.target as any).value = "";
if (this._devices.includes(device)) {
return;
}
this._devices = [...this._devices, device];
const deviceEntities = this._deviceEntityLookup[device];
this._entities = [...this._entities, ...deviceEntities];
deviceEntities.forEach((entityId) => {
this._storeState(entityId);
});
this._dirty = true;
}
private _deleteDevice(ev: Event) {
const deviceId = (ev.target as any).device;
this._devices = this._devices.filter((device) => device !== deviceId);
const deviceEntities = this._deviceEntityLookup[deviceId];
this._entities = this._entities.filter(
(entityId) => !deviceEntities.includes(entityId)
);
this._dirty = true;
}
private _nameChanged(ev: CustomEvent) {
if (!this._config || this._config.name === ev.detail.value) {
return;
}
this._config.name = ev.detail.value;
this._dirty = true;
}
private _stateChanged(event: HassEvent) {
if (
event.context.id !== this._activateContextId &&
this._entities.includes(event.data.entity_id)
) {
this._dirty = true;
}
}
private _backTapped(): void {
if (this._dirty) {
showConfirmationDialog(this, {
text: this.hass!.localize(
"ui.panel.config.scene.editor.unsaved_confirm"
),
confirmBtnText: this.hass!.localize("ui.common.yes"),
cancelBtnText: this.hass!.localize("ui.common.no"),
confirm: () => this._goBack(),
});
} else {
this._goBack();
}
}
private _goBack(): void {
applyScene(this.hass, this._storedStates);
history.back();
}
private _deleteTapped(): void {
showConfirmationDialog(this, {
text: this.hass!.localize("ui.panel.config.scene.picker.delete_confirm"),
confirmBtnText: this.hass!.localize("ui.common.yes"),
cancelBtnText: this.hass!.localize("ui.common.no"),
confirm: () => this._delete(),
});
}
private async _delete(): Promise<void> {
await deleteScene(this.hass, this.scene!.attributes.id!);
applyScene(this.hass, this._storedStates);
history.back();
}
private _calculateStates(): SceneEntities {
const output: SceneEntities = {};
this._entities.forEach((entityId) => {
const state = this._getCurrentState(entityId);
if (state) {
output[entityId] = state;
}
});
return output;
}
private _storeState(entityId: string): void {
if (entityId in this._storedStates) {
return;
}
const state = this._getCurrentState(entityId);
if (!state) {
return;
}
this._storedStates[entityId] = state;
}
private _getCurrentState(entityId: string) {
const stateObj = this.hass.states[entityId];
if (!stateObj) {
return;
}
const domain = computeDomain(entityId);
const attributes = {};
for (const attribute in stateObj.attributes) {
if (
SCENE_SAVED_ATTRIBUTES[domain] &&
SCENE_SAVED_ATTRIBUTES[domain].includes(attribute)
) {
attributes[attribute] = stateObj.attributes[attribute];
}
}
return { ...attributes, state: stateObj.state };
}
private async _saveScene(): Promise<void> {
const id = this.creatingNew ? "" + Date.now() : this.scene!.attributes.id!;
this._config = { ...this._config, entities: this._calculateStates() };
try {
await saveScene(this.hass, id, this._config);
this._dirty = false;
if (this.creatingNew) {
navigate(this, `/config/scene/edit/${id}`, true);
}
} catch (err) {
this._errors = err.body.message || err.message;
throw err;
}
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
ha-card {
overflow: hidden;
}
.errors {
padding: 20px;
font-weight: bold;
color: var(--google-red-500);
}
.content {
padding-bottom: 20px;
}
.triggers,
.script {
margin-top: -16px;
}
.triggers ha-card,
.script ha-card {
margin-top: 16px;
}
.add-card mwc-button {
display: block;
text-align: center;
}
.card-menu {
position: absolute;
top: 0;
right: 0;
z-index: 1;
color: var(--primary-text-color);
}
.rtl .card-menu {
right: auto;
left: 0;
}
.card-menu paper-item {
cursor: pointer;
}
paper-icon-item {
padding: 8px 16px;
}
ha-card paper-icon-button {
color: var(--secondary-text-color);
}
.card-header > paper-icon-button {
float: right;
position: relative;
top: -8px;
}
.device-entity {
cursor: pointer;
}
span[slot="introduction"] a {
color: var(--primary-color);
}
ha-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
margin-bottom: -80px;
transition: margin-bottom 0.3s;
}
ha-fab[is-wide] {
bottom: 24px;
right: 24px;
}
ha-fab[dirty] {
margin-bottom: 0;
}
ha-fab.rtl {
right: auto;
left: 16px;
}
ha-fab[is-wide].rtl {
bottom: 24px;
right: auto;
left: 24px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-scene-editor": HaSceneEditor;
}
}

View File

@ -104,6 +104,7 @@ class HaScriptEditor extends LocalizeMixin(NavigateMixin(PolymerElement)) {
<template is="dom-if" if="[[!creatingNew]]">
<paper-icon-button
icon="hass:delete"
title="[[localize('ui.panel.config.script.editor.delete_script')]]"
on-click="_delete"
></paper-icon-button>
</template>

View File

@ -72,6 +72,9 @@ class HaScriptPicker extends LitElement {
<paper-icon-button
.script=${script}
icon="hass:play"
title="${this.hass.localize(
"ui.panel.config.script.picker.trigger_script"
)}"
@click=${this._runScript}
></paper-icon-button>
<paper-item-body>
@ -81,6 +84,9 @@ class HaScriptPicker extends LitElement {
<a href=${`/config/script/edit/${script.entity_id}`}>
<paper-icon-button
icon="hass:pencil"
title="${this.hass.localize(
"ui.panel.config.script.picker.edit_script"
)}"
></paper-icon-button>
</a>
</div>

View File

@ -9,7 +9,7 @@ import "../../../resources/ha-style";
import "../ha-config-section";
import isComponentLoaded from "../../../common/config/is_component_loaded";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import LocalizeMixin from "../../../mixins/localize-mixin";
/*

View File

@ -78,13 +78,23 @@ export class HaPanelCustom extends UpdatingElement {
!["localhost", "127.0.0.1", location.hostname].includes(tempA.hostname)
) {
if (
!confirm(`Do you trust the external panel "${config.name}" at "${
tempA.href
}"?
!confirm(
`${this.hass.localize(
"ui.panel.custom.external_panel.question_trust",
"name",
config.name,
"link",
tempA.href
)}
It will have access to all data in Home Assistant.
${this.hass.localize(
"ui.panel.custom.external_panel.complete_access"
)}
(Check docs for the panel_custom component to hide this message)`)
(${this.hass.localize(
"ui.panel.custom.external_panel.hide_message"
)})`
)
) {
return;
}

View File

@ -22,7 +22,7 @@ import scrollToTarget from "../../common/dom/scroll-to-target";
import { haStyle } from "../../resources/styles";
import { HomeAssistant, Route } from "../../types";
import { navigate } from "../../common/navigate";
import isComponentLoaded from "../../common/config/is_component_loaded";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
@customElement("ha-panel-developer-tools")
class PanelDeveloperTools extends LitElement {

View File

@ -112,7 +112,7 @@ class HaPanelDevService extends LocalizeMixin(PolymerElement) {
value="[[_computeEntityValue(parsedJSON)]]"
on-change="_entityPicked"
disabled="[[!validJSON]]"
domain-filter="[[_computeEntityDomainFilter(_domain)]]"
include-domains="[[_computeEntityDomainFilter(_domain)]]"
allow-custom-entity
></ha-entity-picker>
</template>
@ -285,7 +285,7 @@ class HaPanelDevService extends LocalizeMixin(PolymerElement) {
}
_computeEntityDomainFilter(domain) {
return ENTITY_COMPONENT_DOMAINS.includes(domain) ? domain : null;
return ENTITY_COMPONENT_DOMAINS.includes(domain) ? [domain] : null;
}
_callService() {

View File

@ -6,13 +6,14 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import formatTime from "../../common/datetime/format_time";
import formatDate from "../../common/datetime/format_date";
import { EventsMixin } from "../../mixins/events-mixin";
import LocalizeMixin from "../../mixins/localize-mixin";
import { domainIcon } from "../../common/entity/domain_icon";
import { computeRTL } from "../../common/util/compute_rtl";
/*
* @appliesMixin EventsMixin
*/
class HaLogbook extends EventsMixin(PolymerElement) {
class HaLogbook extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex"></style>
@ -55,7 +56,7 @@ class HaLogbook extends EventsMixin(PolymerElement) {
</style>
<template is="dom-if" if="[[!entries.length]]">
No logbook entries found.
[[localize('ui.panel.logbook.entries_not_found')]]
</template>
<template is="dom-repeat" items="[[entries]]">

View File

@ -11,11 +11,11 @@ import "../components/hui-warning-element";
import { LovelaceBadge } from "../types";
import { HomeAssistant } from "../../../types";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { StateLabelBadgeConfig } from "./types";
import { longPress } from "../common/directives/long-press-directive";
import { hasDoubleClick } from "../common/has-double-click";
import { handleClick } from "../common/handle-click";
import { actionHandler } from "../common/directives/action-handler-directive";
import { hasAction } from "../common/has-action";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { handleAction } from "../common/handle-action";
@customElement("hui-state-label-badge")
export class HuiStateLabelBadge extends LitElement implements LovelaceBadge {
@ -37,33 +37,20 @@ export class HuiStateLabelBadge extends LitElement implements LovelaceBadge {
<ha-state-label-badge
.hass=${this.hass}
.state=${stateObj}
.title=${this._config.name
? this._config.name
: stateObj
? computeStateName(stateObj)
: ""}
.name=${this._config.name}
.icon=${this._config.icon}
.image=${this._config.image}
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config!.double_tap_action),
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
></ha-state-label-badge>
`;
}
private _handleClick() {
handleClick(this, this.hass!, this._config!, false, false);
}
private _handleHold() {
handleClick(this, this.hass!, this._config!, true, false);
}
private _handleDblClick() {
handleClick(this, this.hass!, this._config!, false, true);
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action!);
}
}

View File

@ -24,11 +24,12 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import { HomeAssistant, LightEntity } from "../../../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { longPress } from "../common/directives/long-press-directive";
import { handleClick } from "../common/handle-click";
import { DOMAINS_TOGGLE } from "../../../common/const";
import { EntityButtonCardConfig } from "./types";
import { hasDoubleClick } from "../common/has-double-click";
import { actionHandler } from "../common/directives/action-handler-directive";
import { hasAction } from "../common/has-action";
import { handleAction } from "../common/handle-action";
import { ActionHandlerEvent } from "../../../data/lovelace";
@customElement("hui-entity-button-card")
class HuiEntityButtonCard extends LitElement implements LovelaceCard {
@ -126,11 +127,10 @@ class HuiEntityButtonCard extends LitElement implements LovelaceCard {
return html`
<ha-card
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config!.double_tap_action),
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
>
${this._config.show_icon
@ -232,16 +232,8 @@ class HuiEntityButtonCard extends LitElement implements LovelaceCard {
return `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
}
private _handleClick() {
handleClick(this, this.hass!, this._config!, false, false);
}
private _handleHold() {
handleClick(this, this.hass!, this._config!, true, false);
}
private _handleDblClick() {
handleClick(this, this.hass!, this._config!, false, true);
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action!);
}
}

View File

@ -22,11 +22,12 @@ import "../components/hui-warning-element";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { HomeAssistant } from "../../../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { longPress } from "../common/directives/long-press-directive";
import { processConfigEntities } from "../common/process-config-entities";
import { handleClick } from "../common/handle-click";
import { GlanceCardConfig, GlanceConfigEntity } from "./types";
import { hasDoubleClick } from "../common/has-double-click";
import { actionHandler } from "../common/directives/action-handler-directive";
import { hasAction } from "../common/has-action";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { handleAction } from "../common/handle-action";
@customElement("hui-glance-card")
export class HuiGlanceCard extends LitElement implements LovelaceCard {
@ -199,11 +200,10 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
<div
class="entity"
.config="${entityConf}"
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(entityConf.double_tap_action),
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(entityConf.hold_action),
hasDoubleClick: hasAction(entityConf.double_tap_action),
})}
>
${this._config!.show_name !== false
@ -245,19 +245,9 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
`;
}
private _handleClick(ev: MouseEvent): void {
private _handleAction(ev: ActionHandlerEvent) {
const config = (ev.currentTarget as any).config as GlanceConfigEntity;
handleClick(this, this.hass!, config, false, false);
}
private _handleHold(ev: MouseEvent): void {
const config = (ev.currentTarget as any).config as GlanceConfigEntity;
handleClick(this, this.hass!, config, true, false);
}
private _handleDblClick(ev: MouseEvent): void {
const config = (ev.currentTarget as any).config as GlanceConfigEntity;
handleClick(this, this.hass!, config, false, true);
handleAction(this, this.hass!, config, ev.detail.action!);
}
}

View File

@ -14,11 +14,12 @@ import "../../../components/ha-card";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { HomeAssistant } from "../../../types";
import { classMap } from "lit-html/directives/class-map";
import { handleClick } from "../common/handle-click";
import { longPress } from "../common/directives/long-press-directive";
import { PictureCardConfig } from "./types";
import { hasDoubleClick } from "../common/has-double-click";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { actionHandler } from "../common/directives/action-handler-directive";
import { hasAction } from "../common/has-action";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { handleAction } from "../common/handle-action";
@customElement("hui-picture-card")
export class HuiPictureCard extends LitElement implements LovelaceCard {
@ -78,11 +79,10 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
return html`
<ha-card
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config!.double_tap_action),
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
class="${classMap({
clickable: Boolean(
@ -112,16 +112,8 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
`;
}
private _handleClick() {
handleClick(this, this.hass!, this._config!, false, false);
}
private _handleHold() {
handleClick(this, this.hass!, this._config!, true, false);
}
private _handleDblClick() {
handleClick(this, this.hass!, this._config!, false, true);
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action!);
}
}

View File

@ -18,15 +18,16 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { longPress } from "../common/directives/long-press-directive";
import { HomeAssistant } from "../../../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { handleClick } from "../common/handle-click";
import { UNAVAILABLE } from "../../../data/entity";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { PictureEntityCardConfig } from "./types";
import { hasDoubleClick } from "../common/has-double-click";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { actionHandler } from "../common/directives/action-handler-directive";
import { hasAction } from "../common/has-action";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { handleAction } from "../common/handle-action";
@customElement("hui-picture-entity-card")
class HuiPictureEntityCard extends LitElement implements LovelaceCard {
@ -146,11 +147,10 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
.cameraView=${this._config.camera_view}
.entity=${this._config.entity}
.aspectRatio=${this._config.aspect_ratio}
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config!.double_tap_action),
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
class=${classMap({
clickable: stateObj.state !== UNAVAILABLE,
@ -202,16 +202,8 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
`;
}
private _handleClick() {
handleClick(this, this.hass!, this._config!, false, false);
}
private _handleHold() {
handleClick(this, this.hass!, this._config!, true, false);
}
private _handleDblClick() {
handleClick(this, this.hass!, this._config!, false, true);
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action!);
}
}

View File

@ -22,13 +22,14 @@ import { computeStateDisplay } from "../../../common/entity/compute_state_displa
import { DOMAINS_TOGGLE } from "../../../common/const";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { HomeAssistant } from "../../../types";
import { longPress } from "../common/directives/long-press-directive";
import { processConfigEntities } from "../common/process-config-entities";
import { handleClick } from "../common/handle-click";
import { hasDoubleClick } from "../common/has-double-click";
import { PictureGlanceCardConfig, PictureGlanceEntityConfig } from "./types";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { actionHandler } from "../common/directives/action-handler-directive";
import { hasAction } from "../common/has-action";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { handleAction } from "../common/handle-action";
const STATES_OFF = new Set(["closed", "locked", "not_home", "off"]);
@ -160,11 +161,10 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
this._config.camera_image
),
})}
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config!.double_tap_action),
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
.config=${this._config}
.hass=${this.hass}
@ -223,11 +223,10 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
return html`
<div class="wrapper">
<ha-icon
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(entityConf.double_tap_action),
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(entityConf.hold_action),
hasDoubleClick: hasAction(entityConf.double_tap_action),
})}
.config=${entityConf}
class="${classMap({
@ -259,19 +258,9 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
`;
}
private _handleClick(ev: MouseEvent): void {
private _handleAction(ev: ActionHandlerEvent) {
const config = (ev.currentTarget as any).config as any;
handleClick(this, this.hass!, config, false, false);
}
private _handleHold(ev: MouseEvent): void {
const config = (ev.currentTarget as any).config as any;
handleClick(this, this.hass!, config, true, false);
}
private _handleDblClick(ev: MouseEvent): void {
const config = (ev.currentTarget as any).config as any;
handleClick(this, this.hass!, config, false, true);
handleAction(this, this.hass!, config, ev.detail.action!);
}
static get styles(): CSSResult {

View File

@ -1,21 +1,31 @@
import { directive, PropertyPart } from "lit-html";
import "@material/mwc-ripple";
import { LongPressOptions } from "../../../../data/lovelace";
import {
ActionHandlerOptions,
ActionHandlerDetail,
} from "../../../../data/lovelace";
import { fireEvent } from "../../../../common/dom/fire_event";
const isTouch =
"ontouchstart" in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0;
interface LongPress extends HTMLElement {
interface ActionHandler extends HTMLElement {
holdTime: number;
bind(element: Element, options): void;
}
interface LongPressElement extends Element {
longPress?: boolean;
interface ActionHandlerElement extends HTMLElement {
actionHandler?: boolean;
}
class LongPress extends HTMLElement implements LongPress {
declare global {
interface HASSDomEvents {
action: ActionHandlerDetail;
}
}
class ActionHandler extends HTMLElement implements ActionHandler {
public holdTime: number;
public ripple: any;
protected timer: number | undefined;
@ -67,11 +77,11 @@ class LongPress extends HTMLElement implements LongPress {
});
}
public bind(element: LongPressElement, options) {
if (element.longPress) {
public bind(element: ActionHandlerElement, options) {
if (element.actionHandler) {
return;
}
element.longPress = true;
element.actionHandler = true;
element.addEventListener("contextmenu", (ev: Event) => {
const e = ev || window.event;
@ -100,10 +110,13 @@ class LongPress extends HTMLElement implements LongPress {
x = (ev as MouseEvent).pageX;
y = (ev as MouseEvent).pageY;
}
this.timer = window.setTimeout(() => {
this.startAnimation(x, y);
this.held = true;
}, this.holdTime);
if (options.hasHold) {
this.timer = window.setTimeout(() => {
this.startAnimation(x, y);
this.held = true;
}, this.holdTime);
}
this.cooldownStart = true;
window.setTimeout(() => (this.cooldownStart = false), 100);
@ -121,18 +134,18 @@ class LongPress extends HTMLElement implements LongPress {
this.stopAnimation();
this.timer = undefined;
if (this.held) {
element.dispatchEvent(new Event("ha-hold"));
fireEvent(element, "action", { action: "hold" });
} else if (options.hasDoubleClick) {
if ((ev as MouseEvent).detail === 1) {
if ((ev as MouseEvent).detail === 1 || ev.type === "keyup") {
this.dblClickTimeout = window.setTimeout(() => {
element.dispatchEvent(new Event("ha-click"));
fireEvent(element, "action", { action: "tap" });
}, 250);
} else {
clearTimeout(this.dblClickTimeout);
element.dispatchEvent(new Event("ha-dblclick"));
fireEvent(element, "action", { action: "double_tap" });
}
} else {
element.dispatchEvent(new Event("ha-click"));
fireEvent(element, "action", { action: "tap" });
}
this.cooldownEnd = true;
window.setTimeout(() => (this.cooldownEnd = false), 100);
@ -150,7 +163,7 @@ class LongPress extends HTMLElement implements LongPress {
element.addEventListener("keyup", handleEnter);
// iOS 13 sends a complete normal touchstart-touchend series of events followed by a mousedown-click series.
// That might be a bug, but until it's fixed, this should make long-press work.
// That might be a bug, but until it's fixed, this should make action-handler work.
// If it's not a bug that is fixed, this might need updating with the next iOS version.
// Note that all events (both touch and mouse) must be listened for in order to work on computers with both mouse and touchscreen.
const isIOS13 = window.navigator.userAgent.match(/iPhone OS 13_/);
@ -178,33 +191,33 @@ class LongPress extends HTMLElement implements LongPress {
}
}
customElements.define("long-press", LongPress);
customElements.define("action-handler", ActionHandler);
const getLongPress = (): LongPress => {
const geActionHandler = (): ActionHandler => {
const body = document.body;
if (body.querySelector("long-press")) {
return body.querySelector("long-press") as LongPress;
if (body.querySelector("action-handler")) {
return body.querySelector("action-handler") as ActionHandler;
}
const longpress = document.createElement("long-press");
body.appendChild(longpress);
const actionhandler = document.createElement("action-handler");
body.appendChild(actionhandler);
return longpress as LongPress;
return actionhandler as ActionHandler;
};
export const longPressBind = (
element: LongPressElement,
options: LongPressOptions
export const actionHandlerBind = (
element: ActionHandlerElement,
options: ActionHandlerOptions
) => {
const longpress: LongPress = getLongPress();
if (!longpress) {
const actionhandler: ActionHandler = geActionHandler();
if (!actionhandler) {
return;
}
longpress.bind(element, options);
actionhandler.bind(element, options);
};
export const longPress = directive(
(options: LongPressOptions = {}) => (part: PropertyPart) => {
longPressBind(part.committer.element, options);
export const actionHandler = directive(
(options: ActionHandlerOptions = {}) => (part: PropertyPart) => {
actionHandlerBind(part.committer.element as ActionHandlerElement, options);
}
);

View File

@ -1,10 +1,10 @@
import { STATES_OFF } from "../../../../common/const";
import { turnOnOffEntity } from "./turn-on-off-entity";
import { HomeAssistant } from "../../../../types";
import { HomeAssistant, ServiceCallResponse } from "../../../../types";
export const toggleEntity = (
hass: HomeAssistant,
entityId: string
): Promise<void> => {
): Promise<ServiceCallResponse> => {
const turnOn = STATES_OFF.includes(hass.states[entityId].state);
return turnOnOffEntity(hass, entityId, turnOn);
};

View File

@ -1,11 +1,11 @@
import { computeDomain } from "../../../../common/entity/compute_domain";
import { HomeAssistant } from "../../../../types";
import { HomeAssistant, ServiceCallResponse } from "../../../../types";
export const turnOnOffEntity = (
hass: HomeAssistant,
entityId: string,
turnOn = true
): Promise<void> => {
): Promise<ServiceCallResponse> => {
const stateDomain = computeDomain(entityId);
const serviceDomain = stateDomain === "group" ? "homeassistant" : stateDomain;

View File

@ -1,11 +1,11 @@
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate";
import { toggleEntity } from "../../../../src/panels/lovelace/common/entity/toggle-entity";
import { toggleEntity } from "./entity/toggle-entity";
import { ActionConfig } from "../../../data/lovelace";
import { forwardHaptic } from "../../../data/haptics";
export const handleClick = (
export const handleAction = (
node: HTMLElement,
hass: HomeAssistant,
config: {
@ -15,16 +15,15 @@ export const handleClick = (
tap_action?: ActionConfig;
double_tap_action?: ActionConfig;
},
hold: boolean,
dblClick: boolean
action: string
): void => {
let actionConfig: ActionConfig | undefined;
if (dblClick && config.double_tap_action) {
if (action === "double_tap" && config.double_tap_action) {
actionConfig = config.double_tap_action;
} else if (hold && config.hold_action) {
} else if (action === "hold" && config.hold_action) {
actionConfig = config.hold_action;
} else if (!hold && config.tap_action) {
} else if (action === "tap" && config.tap_action) {
actionConfig = config.tap_action;
}
@ -41,6 +40,8 @@ export const handleClick = (
(e) => e.user === hass!.user!.id
))
) {
forwardHaptic("warning");
if (
!confirm(
actionConfig.confirmation.text ||

View File

@ -1,6 +1,5 @@
import { ActionConfig } from "../../../data/lovelace";
// Check if config or Entity changed
export function hasDoubleClick(config?: ActionConfig): boolean {
export function hasAction(config?: ActionConfig): boolean {
return config !== undefined && config.action !== "none";
}

View File

@ -76,7 +76,7 @@ export class HuiCardOptions extends LitElement {
"ui.panel.lovelace.editor.edit_card.move"
)}</paper-item
>
<paper-item @click="${this._deleteCard}">
<paper-item .class="delete-item" @click="${this._deleteCard}">
${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.delete"
)}</paper-item
@ -133,6 +133,10 @@ export class HuiCardOptions extends LitElement {
paper-item {
cursor: pointer;
}
paper-item.delete-item {
color: var(--google-red-500);
}
`;
}

View File

@ -17,13 +17,14 @@ import "../components/hui-warning";
import { HomeAssistant } from "../../../types";
import { computeRTL } from "../../../common/util/compute_rtl";
import { toggleAttribute } from "../../../common/dom/toggle_attribute";
import { longPress } from "../common/directives/long-press-directive";
import { hasDoubleClick } from "../common/has-double-click";
import { handleClick } from "../common/handle-click";
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
import { computeDomain } from "../../../common/entity/compute_domain";
import { classMap } from "lit-html/directives/class-map";
import { EntitiesCardEntityConfig } from "../cards/types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { hasAction } from "../common/has-action";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { handleAction } from "../common/handle-action";
class HuiGenericEntityRow extends LitElement {
@property() public hass?: HomeAssistant;
@ -66,11 +67,10 @@ class HuiGenericEntityRow extends LitElement {
.stateObj=${stateObj}
.overrideIcon=${this.config.icon}
.overrideImage=${this.config.image}
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this.config.double_tap_action),
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this.config!.hold_action),
hasDoubleClick: hasAction(this.config!.double_tap_action),
})}
tabindex="0"
></state-badge>
@ -84,11 +84,10 @@ class HuiGenericEntityRow extends LitElement {
!this.showSecondary || this.config.secondary_info
),
})}
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this.config.double_tap_action),
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this.config!.hold_action),
hasDoubleClick: hasAction(this.config!.double_tap_action),
})}
>
${this.config.name || computeStateName(stateObj)}
@ -122,16 +121,8 @@ class HuiGenericEntityRow extends LitElement {
}
}
private _handleClick(): void {
handleClick(this, this.hass!, this.config!, false, false);
}
private _handleHold(): void {
handleClick(this, this.hass!, this.config!, true, false);
}
private _handleDblClick(): void {
handleClick(this, this.hass!, this.config!, false, true);
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this.config!, ev.detail.action!);
}
static get styles(): CSSResult {

View File

@ -163,7 +163,7 @@ export class HuiDialogEditCard extends LitElement {
}
}
@media all and (min-width: 660px) {
@media all and (min-width: 850px) {
ha-paper-dialog {
width: 845px;
}

View File

@ -78,7 +78,7 @@ export class HuiAlarmPanelCardEditor extends LitElement
.hass="${this.hass}"
.value="${this._entity}"
.configValue=${"entity"}
domain-filter="alarm_control_panel"
include-domains='["alarm_control_panel"]'
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>

View File

@ -92,7 +92,7 @@ export class HuiGaugeCardEditor extends LitElement
.hass="${this.hass}"
.value="${this._entity}"
.configValue=${"entity"}
domain-filter="sensor"
include-domains='["sensor"]'
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>

View File

@ -71,7 +71,7 @@ export class HuiLightCardEditor extends LitElement
.hass="${this.hass}"
.value="${this._entity}"
.configValue=${"entity"}
domain-filter="light"
include-domains='["light"]'
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>

View File

@ -52,7 +52,7 @@ export class HuiMediaControlCardEditor extends LitElement
.hass="${this.hass}"
.value="${this._entity}"
.configValue=${"entity"}
domain-filter="media_player"
include-domains='["media_player"]'
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>

View File

@ -152,7 +152,7 @@ export class HuiPictureEntityCardEditor extends LitElement
.value="${this._camera_image}"
.configValue=${"camera_image"}
@change="${this._valueChanged}"
domain-filter="camera"
include-domains='["camera"]'
allow-custom-entity
></ha-entity-picker>
<div class="side-by-side">

View File

@ -152,7 +152,7 @@ export class HuiPictureGlanceCardEditor extends LitElement
.configValue=${"camera_image"}
@change="${this._valueChanged}"
allow-custom-entity
domain-filter="camera"
include-domains='["camera"]'
></ha-entity-picker>
<div class="side-by-side">
<paper-dropdown-menu

View File

@ -67,7 +67,7 @@ export class HuiPlantStatusCardEditor extends LitElement
.hass="${this.hass}"
.value="${this._entity}"
.configValue=${"entity"}
domain-filter="plant"
include-domains='["plant"]'
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>

View File

@ -96,7 +96,7 @@ export class HuiSensorCardEditor extends LitElement
.hass="${this.hass}"
.value="${this._entity}"
.configValue=${"entity"}
domain-filter="sensor"
include-domains='["sensor"]'
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>

View File

@ -66,7 +66,7 @@ export class HuiThermostatCardEditor extends LitElement
.hass="${this.hass}"
.value="${this._entity}"
.configValue=${"entity"}
domain-filter="climate"
include-domains='["climate"]'
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>

View File

@ -65,7 +65,7 @@ export class HuiWeatherForecastCardEditor extends LitElement
.hass="${this.hass}"
.value="${this._entity}"
.configValue=${"entity"}
domain-filter="weather"
include-domains='["weather"]'
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>

View File

@ -57,7 +57,7 @@ export class HuiUnusedEntities extends LitElement {
private _columns = memoizeOne((narrow: boolean) => {
const columns: DataTableColumnContainer = {
entity: {
title: "Entity",
title: this.hass!.localize("ui.panel.lovelace.unused_entities.entity"),
sortable: true,
filterable: true,
filterKey: "friendly_name",
@ -79,17 +79,19 @@ export class HuiUnusedEntities extends LitElement {
}
columns.entity_id = {
title: "Entity id",
title: this.hass!.localize("ui.panel.lovelace.unused_entities.entity_id"),
sortable: true,
filterable: true,
};
columns.domain = {
title: "Domain",
title: this.hass!.localize("ui.panel.lovelace.unused_entities.domain"),
sortable: true,
filterable: true,
};
columns.last_changed = {
title: "Last Changed",
title: this.hass!.localize(
"ui.panel.lovelace.unused_entities.last_changed"
),
type: "numeric",
sortable: true,
template: (lastChanged: string) => html`
@ -121,14 +123,20 @@ export class HuiUnusedEntities extends LitElement {
}
return html`
<ha-card header="Unused entities">
<ha-card
header="${this.hass.localize(
"ui.panel.lovelace.unused_entities.title"
)}"
>
<div class="card-content">
These are the entities that you have available, but are not in your
Lovelace UI yet.
${this.hass.localize(
"ui.panel.lovelace.unused_entities.available_entities"
)}
${this.lovelace.mode === "storage"
? html`
<br />Select the entities you want to add to a card and then
click the add card button.
<br />${this.hass.localize(
"ui.panel.lovelace.unused_entities.select_to_add"
)}
`
: ""}
</div>

View File

@ -11,11 +11,12 @@ import {
import "../../../components/ha-icon";
import { computeTooltip } from "../common/compute-tooltip";
import { handleClick } from "../common/handle-click";
import { longPress } from "../common/directives/long-press-directive";
import { LovelaceElement, IconElementConfig } from "./types";
import { HomeAssistant } from "../../../types";
import { hasDoubleClick } from "../common/has-double-click";
import { actionHandler } from "../common/directives/action-handler-directive";
import { hasAction } from "../common/has-action";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { handleAction } from "../common/handle-action";
@customElement("hui-icon-element")
export class HuiIconElement extends LitElement implements LovelaceElement {
@ -39,26 +40,17 @@ export class HuiIconElement extends LitElement implements LovelaceElement {
<ha-icon
.icon="${this._config.icon}"
.title="${computeTooltip(this.hass, this._config)}"
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config!.double_tap_action),
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
></ha-icon>
`;
}
private _handleClick(): void {
handleClick(this, this.hass!, this._config!, false, false);
}
private _handleHold(): void {
handleClick(this, this.hass!, this._config!, true, false);
}
private _handleDblClick() {
handleClick(this, this.hass!, this._config!, false, true);
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action!);
}
static get styles(): CSSResult {

View File

@ -11,11 +11,12 @@ import {
import "../components/hui-image";
import { computeTooltip } from "../common/compute-tooltip";
import { handleClick } from "../common/handle-click";
import { longPress } from "../common/directives/long-press-directive";
import { LovelaceElement, ImageElementConfig } from "./types";
import { HomeAssistant } from "../../../types";
import { hasDoubleClick } from "../common/has-double-click";
import { actionHandler } from "../common/directives/action-handler-directive";
import { hasAction } from "../common/has-action";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { handleAction } from "../common/handle-action";
@customElement("hui-image-element")
export class HuiImageElement extends LitElement implements LovelaceElement {
@ -50,11 +51,10 @@ export class HuiImageElement extends LitElement implements LovelaceElement {
.stateFilter="${this._config.state_filter}"
.title="${computeTooltip(this.hass, this._config)}"
.aspectRatio="${this._config.aspect_ratio}"
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config!.double_tap_action),
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
></hui-image>
`;
@ -73,16 +73,8 @@ export class HuiImageElement extends LitElement implements LovelaceElement {
`;
}
private _handleClick(): void {
handleClick(this, this.hass!, this._config!, false, false);
}
private _handleHold(): void {
handleClick(this, this.hass!, this._config!, true, false);
}
private _handleDblClick() {
handleClick(this, this.hass!, this._config!, false, true);
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action!);
}
}

View File

@ -14,9 +14,10 @@ import { computeStateName } from "../../../common/entity/compute_state_name";
import { LovelaceElement, StateBadgeElementConfig } from "./types";
import { HomeAssistant } from "../../../types";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { longPress } from "../common/directives/long-press-directive";
import { hasDoubleClick } from "../common/has-double-click";
import { handleClick } from "../common/handle-click";
import { actionHandler } from "../common/directives/action-handler-directive";
import { hasAction } from "../common/has-action";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { handleAction } from "../common/handle-action";
@customElement("hui-state-badge-element")
export class HuiStateBadgeElement extends LitElement
@ -64,26 +65,17 @@ export class HuiStateBadgeElement extends LitElement
: this._config.title === null
? ""
: this._config.title}"
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config!.double_tap_action),
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
></ha-state-label-badge>
`;
}
private _handleClick() {
handleClick(this, this.hass!, this._config!, false, false);
}
private _handleHold() {
handleClick(this, this.hass!, this._config!, true, false);
}
private _handleDblClick() {
handleClick(this, this.hass!, this._config!, false, true);
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action!);
}
}

View File

@ -13,12 +13,13 @@ import "../../../components/entity/state-badge";
import "../components/hui-warning-element";
import { computeTooltip } from "../common/compute-tooltip";
import { handleClick } from "../common/handle-click";
import { longPress } from "../common/directives/long-press-directive";
import { LovelaceElement, StateIconElementConfig } from "./types";
import { HomeAssistant } from "../../../types";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { hasDoubleClick } from "../common/has-double-click";
import { actionHandler } from "../common/directives/action-handler-directive";
import { hasAction } from "../common/has-action";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { handleAction } from "../common/handle-action";
@customElement("hui-state-icon-element")
export class HuiStateIconElement extends LitElement implements LovelaceElement {
@ -60,11 +61,10 @@ export class HuiStateIconElement extends LitElement implements LovelaceElement {
<state-badge
.stateObj="${stateObj}"
.title="${computeTooltip(this.hass, this._config)}"
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config!.double_tap_action),
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
.overrideIcon=${this._config.icon}
></state-badge>
@ -79,16 +79,8 @@ export class HuiStateIconElement extends LitElement implements LovelaceElement {
`;
}
private _handleClick(): void {
handleClick(this, this.hass!, this._config!, false, false);
}
private _handleHold(): void {
handleClick(this, this.hass!, this._config!, true, false);
}
private _handleDblClick() {
handleClick(this, this.hass!, this._config!, false, true);
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action!);
}
}

View File

@ -13,12 +13,13 @@ import "../components/hui-warning-element";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { computeTooltip } from "../common/compute-tooltip";
import { handleClick } from "../common/handle-click";
import { longPress } from "../common/directives/long-press-directive";
import { LovelaceElement, StateLabelElementConfig } from "./types";
import { HomeAssistant } from "../../../types";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { hasDoubleClick } from "../common/has-double-click";
import { actionHandler } from "../common/directives/action-handler-directive";
import { hasAction } from "../common/has-action";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { handleAction } from "../common/handle-action";
@customElement("hui-state-label-element")
class HuiStateLabelElement extends LitElement implements LovelaceElement {
@ -59,11 +60,10 @@ class HuiStateLabelElement extends LitElement implements LovelaceElement {
return html`
<div
.title="${computeTooltip(this.hass, this._config)}"
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config!.double_tap_action),
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
>
${this._config.prefix}${stateObj
@ -77,16 +77,8 @@ class HuiStateLabelElement extends LitElement implements LovelaceElement {
`;
}
private _handleClick(): void {
handleClick(this, this.hass!, this._config!, false, false);
}
private _handleHold(): void {
handleClick(this, this.hass!, this._config!, true, false);
}
private _handleDblClick() {
handleClick(this, this.hass!, this._config!, false, true);
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action!);
}
static get styles(): CSSResult {

View File

@ -23,13 +23,14 @@ import { setInputSelectOption } from "../../../data/input-select";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { forwardHaptic } from "../../../data/haptics";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { longPress } from "../common/directives/long-press-directive";
import { hasDoubleClick } from "../common/has-double-click";
import { handleClick } from "../common/handle-click";
import { classMap } from "lit-html/directives/class-map";
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
import { computeDomain } from "../../../common/entity/compute_domain";
import { EntitiesCardEntityConfig } from "../cards/types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { hasAction } from "../common/has-action";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { handleAction } from "../common/handle-action";
@customElement("hui-input-select-entity-row")
class HuiInputSelectEntityRow extends LitElement implements EntityRow {
@ -81,11 +82,10 @@ class HuiInputSelectEntityRow extends LitElement implements EntityRow {
class=${classMap({
pointer,
})}
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config.double_tap_action),
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
tabindex="0"
></state-badge>
@ -127,16 +127,8 @@ class HuiInputSelectEntityRow extends LitElement implements EntityRow {
)!.selected = stateObj.attributes.options.indexOf(stateObj.state);
}
private _handleClick(): void {
handleClick(this, this.hass!, this._config!, false, false);
}
private _handleHold(): void {
handleClick(this, this.hass!, this._config!, true, false);
}
private _handleDblClick(): void {
handleClick(this, this.hass!, this._config!, false, true);
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action!);
}
static get styles(): CSSResult {

View File

@ -16,10 +16,11 @@ import "../components/hui-warning";
import { HomeAssistant } from "../../../types";
import { EntityRow, EntityConfig } from "./types";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { activateScene } from "../../../data/scene";
@customElement("hui-scene-entity-row")
class HuiSceneEntityRow extends LitElement implements EntityRow {
@property() public hass?: HomeAssistant;
@property() public hass!: HomeAssistant;
@property() private _config?: EntityConfig;
@ -79,11 +80,9 @@ class HuiSceneEntityRow extends LitElement implements EntityRow {
`;
}
private _callService(ev): void {
private _callService(ev: Event): void {
ev.stopPropagation();
this.hass!.callService("scene", "turn_on", {
entity_id: this._config!.entity,
});
activateScene(this.hass, this._config!.entity);
}
}

View File

@ -74,7 +74,10 @@ class LovelacePanel extends LitElement {
if (state === "error") {
return html`
<hass-error-screen title="Lovelace" .error="${this._errorMsg}">
<hass-error-screen
title="${this.hass!.localize("domain.lovelace")}"
.error="${this._errorMsg}"
>
<mwc-button on-click="_forceFetchConfig"
>${this.hass!.localize(
"ui.panel.lovelace.reload_lovelace"

View File

@ -157,7 +157,11 @@ class LovelaceFullConfigEditor extends LitElement {
private _closeEditor() {
if (this._changed) {
if (
!confirm("You have unsaved changes, are you sure you want to exit?")
!confirm(
this.hass.localize(
"ui.panel.lovelace.editor.raw_editor.confirm_unsaved_changes"
)
)
) {
return;
}
@ -174,7 +178,9 @@ class LovelaceFullConfigEditor extends LitElement {
if (this.yamlEditor.hasComments) {
if (
!confirm(
"Your config contains comment(s), these will not be saved. Do you want to continue?"
this.hass.localize(
"ui.panel.lovelace.editor.raw_editor.confirm_unsaved_comments"
)
)
) {
return;
@ -185,20 +191,38 @@ class LovelaceFullConfigEditor extends LitElement {
try {
value = safeLoad(this.yamlEditor.value);
} catch (err) {
alert(`Unable to parse YAML: ${err}`);
alert(
this.hass.localize(
"ui.panel.lovelace.editor.raw_editor.error_parse_yaml",
"error",
err
)
);
this._saving = false;
return;
}
try {
value = lovelaceStruct(value);
} catch (err) {
alert(`Your config is not valid: ${err}`);
alert(
this.hass.localize(
"ui.panel.lovelace.editor.raw_editor.error_invalid_config",
"error",
err
)
);
return;
}
try {
await this.lovelace!.saveConfig(value);
} catch (err) {
alert(`Unable to save YAML: ${err}`);
alert(
this.hass.localize(
"ui.panel.lovelace.editor.raw_editor.error_save_yaml",
"error",
err
)
);
}
this._generation = this.yamlEditor
.codemirror!.getDoc()

View File

@ -24,7 +24,6 @@ import "@polymer/paper-tabs/paper-tabs";
import scrollToTarget from "../../common/dom/scroll-to-target";
import "../../layouts/ha-app-layout";
import "../../components/ha-start-voice-button";
import "../../components/ha-paper-icon-button-arrow-next";
import "../../components/ha-paper-icon-button-arrow-prev";
import "../../components/ha-icon";
@ -49,9 +48,12 @@ import { afterNextRender } from "../../common/util/render-status";
import { haStyle } from "../../resources/styles";
import { computeRTLDirection } from "../../common/util/compute_rtl";
import { loadLovelaceResources } from "./common/load-resources";
import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import memoizeOne from "memoize-one";
class HUIRoot extends LitElement {
@property() public hass?: HomeAssistant;
@property() public hass!: HomeAssistant;
@property() public lovelace?: Lovelace;
@property() public columns?: number;
@property() public narrow?: boolean;
@ -62,6 +64,10 @@ class HUIRoot extends LitElement {
private _debouncedConfigChanged: () => void;
private _conversation = memoizeOne((_components) =>
isComponentLoaded(this.hass, "conversation")
);
constructor() {
super();
// The view can trigger a re-render when it knows that certain
@ -87,6 +93,9 @@ class HUIRoot extends LitElement {
? html`
<app-toolbar class="edit-mode">
<paper-icon-button
title="${this.hass!.localize(
"ui.panel.lovelace.menu.close"
)}"
icon="hass:close"
@click="${this._editModeDisable}"
></paper-icon-button>
@ -94,6 +103,9 @@ class HUIRoot extends LitElement {
${this.config.title ||
this.hass!.localize("ui.panel.lovelace.editor.header")}
<paper-icon-button
title="${this.hass!.localize(
"ui.panel.lovelace.editor.edit_lovelace.edit_title"
)}"
icon="hass:pencil"
class="edit-icon"
@click="${this._editLovelace}"
@ -101,7 +113,9 @@ class HUIRoot extends LitElement {
</div>
<paper-icon-button
icon="hass:help-circle"
title="Help"
title="${this.hass!.localize(
"ui.panel.lovelace.menu.help"
)}"
@click="${this._handleHelp}"
></paper-icon-button>
<paper-menu-button
@ -113,6 +127,9 @@ class HUIRoot extends LitElement {
aria-label=${this.hass!.localize(
"ui.panel.lovelace.editor.menu.open"
)}
title="${this.hass!.localize(
"ui.panel.lovelace.editor.menu.open"
)}"
icon="hass:dots-vertical"
slot="dropdown-trigger"
></paper-icon-button>
@ -125,12 +142,12 @@ class HUIRoot extends LitElement {
: html`
<paper-item
aria-label=${this.hass!.localize(
"ui.panel.lovelace.menu.unused_entities"
"ui.panel.lovelace.unused_entities.title"
)}
@tap="${this._handleUnusedEntities}"
>
${this.hass!.localize(
"ui.panel.lovelace.menu.unused_entities"
"ui.panel.lovelace.unused_entities.title"
)}
</paper-item>
`}
@ -150,9 +167,15 @@ class HUIRoot extends LitElement {
.narrow=${this.narrow}
></ha-menu-button>
<div main-title>${this.config.title || "Home Assistant"}</div>
<ha-start-voice-button
.hass="${this.hass}"
></ha-start-voice-button>
${this._conversation(this.hass.config.components)
? html`
<paper-icon-button
aria-label="Start conversation"
icon="hass:microphone"
@click=${this._showVoiceCommandDialog}
></paper-icon-button>
`
: ""}
<paper-menu-button
no-animations
horizontal-align="right"
@ -180,12 +203,12 @@ class HUIRoot extends LitElement {
</paper-item>
<paper-item
aria-label=${this.hass!.localize(
"ui.panel.lovelace.menu.unused_entities"
"ui.panel.lovelace.unused_entities.title"
)}
@tap="${this._handleUnusedEntities}"
>
${this.hass!.localize(
"ui.panel.lovelace.menu.unused_entities"
"ui.panel.lovelace.unused_entities.title"
)}
</paper-item>
`
@ -247,7 +270,9 @@ class HUIRoot extends LitElement {
${this._editMode
? html`
<ha-paper-icon-button-arrow-prev
title="Move view left"
title="${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.move_left"
)}"
class="edit-icon view"
@click="${this._moveViewLeft}"
?disabled="${this._curView === 0}"
@ -265,13 +290,17 @@ class HUIRoot extends LitElement {
${this._editMode
? html`
<ha-icon
title="Edit view"
title="${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.edit"
)}"
class="edit-icon view"
icon="hass:pencil"
@click="${this._editView}"
></ha-icon>
<ha-paper-icon-button-arrow-next
title="Move view right"
title="${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.move_right"
)}"
class="edit-icon view"
@click="${this._moveViewRight}"
?disabled="${(this._curView! as number) +
@ -529,6 +558,10 @@ class HUIRoot extends LitElement {
ev.target.selected = null;
}
private _showVoiceCommandDialog(): void {
showVoiceCommandDialog(this);
}
private _handleHelp(): void {
window.open("https://www.home-assistant.io/lovelace/", "_blank");
}

View File

@ -3,7 +3,7 @@ import "@polymer/iron-label/iron-label";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import isComponentLoaded from "../../common/config/is_component_loaded";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { pushSupported } from "../../components/ha-push-notifications-toggle";
import LocalizeMixin from "../../mixins/localize-mixin";

View File

@ -13,9 +13,10 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../components/ha-menu-button";
import "../../components/ha-start-voice-button";
import "../../components/ha-card";
import LocalizeMixin from "../../mixins/localize-mixin";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
/*
* @appliesMixin LocalizeMixin
@ -72,10 +73,14 @@ class HaPanelShoppingList extends LocalizeMixin(PolymerElement) {
narrow="[[narrow]]"
></ha-menu-button>
<div main-title>[[localize('panel.shopping_list')]]</div>
<ha-start-voice-button
hass="[[hass]]"
can-listen="{{canListen}}"
></ha-start-voice-button>
<paper-icon-button
hidden$="[[!conversation]]"
aria-label="Start conversation"
icon="hass:microphone"
on-click="_showVoiceCommandDialog"
></paper-icon-button>
<paper-menu-button
horizontal-align="right"
horizontal-offset="-5"
@ -131,7 +136,7 @@ class HaPanelShoppingList extends LocalizeMixin(PolymerElement) {
</paper-icon-item>
</template>
</ha-card>
<div class="tip" hidden$="[[!canListen]]">
<div class="tip" hidden$="[[!conversation]]">
[[localize('ui.panel.shopping-list.microphone_tip')]]
</div>
</div>
@ -143,7 +148,10 @@ class HaPanelShoppingList extends LocalizeMixin(PolymerElement) {
return {
hass: Object,
narrow: Boolean,
canListen: Boolean,
conversation: {
type: Boolean,
computed: "_computeConversation(hass)",
},
items: {
type: Array,
value: [],
@ -207,6 +215,14 @@ class HaPanelShoppingList extends LocalizeMixin(PolymerElement) {
}
}
_computeConversation(hass) {
return isComponentLoaded(hass, "conversation");
}
_showVoiceCommandDialog() {
showVoiceCommandDialog(this);
}
_saveEdit(ev) {
const { index, item } = ev.model;
const name = ev.target.value;

View File

@ -12,7 +12,6 @@ import "@polymer/paper-tabs/paper-tabs";
import "../../components/ha-cards";
import "../../components/ha-icon";
import "../../components/ha-menu-button";
import "../../components/ha-start-voice-button";
import "../../layouts/ha-app-layout";
@ -23,6 +22,8 @@ import { computeStateDomain } from "../../common/entity/compute_state_domain";
import computeLocationName from "../../common/config/location_name";
import NavigateMixin from "../../mixins/navigate-mixin";
import { EventsMixin } from "../../mixins/events-mixin";
import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
const ALWAYS_SHOW_DOMAIN = ["persistent_notification", "configurator"];
@ -72,7 +73,12 @@ class PartialCards extends EventsMixin(NavigateMixin(PolymerElement)) {
<div main-title="">
[[computeTitle(views, defaultView, locationName)]]
</div>
<ha-start-voice-button hass="[[hass]]"></ha-start-voice-button>
<paper-icon-button
hidden$="[[!conversation]]"
aria-label="Start conversation"
icon="hass:microphone"
on-click="_showVoiceCommandDialog"
></paper-icon-button>
</app-toolbar>
<div sticky="" hidden$="[[areTabsHidden(views, showTabs)]]">
@ -174,6 +180,11 @@ class PartialCards extends EventsMixin(NavigateMixin(PolymerElement)) {
value: 1,
},
conversation: {
type: Boolean,
computed: "_computeConversation(hass)",
},
locationName: {
type: String,
value: "",
@ -241,6 +252,14 @@ class PartialCards extends EventsMixin(NavigateMixin(PolymerElement)) {
);
}
_computeConversation(hass) {
return isComponentLoaded(hass, "conversation");
}
_showVoiceCommandDialog() {
showVoiceCommandDialog(this);
}
areTabsHidden(views, showTabs) {
return !views || !views.length || !showTabs;
}

View File

@ -5,6 +5,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/entity/state-info";
import LocalizeMixin from "../mixins/localize-mixin";
import { activateScene } from "../data/scene";
/*
* @appliesMixin LocalizeMixin
@ -23,7 +24,7 @@ class StateCardScene extends LocalizeMixin(PolymerElement) {
<div class="horizontal justified layout">
${this.stateInfoTemplate}
<mwc-button on-click="activateScene"
<mwc-button on-click="_activateScene"
>[[localize('ui.card.scene.activate')]]</mwc-button
>
</div>
@ -51,11 +52,9 @@ class StateCardScene extends LocalizeMixin(PolymerElement) {
};
}
activateScene(ev) {
_activateScene(ev) {
ev.stopPropagation();
this.hass.callService("scene", "turn_on", {
entity_id: this.stateObj.entity_id,
});
activateScene(this.hass, this.stateObj.entity_id);
}
}
customElements.define("state-card-scene", StateCardScene);

View File

@ -17,7 +17,7 @@ import hassCallApi from "../util/hass-call-api";
import { subscribePanels } from "../data/ws-panels";
import { forwardHaptic } from "../data/haptics";
import { fireEvent } from "../common/dom/fire_event";
import { Constructor } from "../types";
import { Constructor, ServiceCallResponse } from "../types";
import { HassBaseEl } from "./hass-base-mixin";
import { broadcastConnectionStatus } from "../data/connection-status";
@ -54,7 +54,12 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
console.log("Calling service", domain, service, serviceData);
}
try {
await callService(conn, domain, service, serviceData);
return (await callService(
conn,
domain,
service,
serviceData
)) as Promise<ServiceCallResponse>;
} catch (err) {
if (__DEV__) {
// tslint:disable-next-line: no-console

View File

@ -517,6 +517,8 @@
"loading": "Loading",
"cancel": "Cancel",
"save": "Save",
"yes": "Yes",
"no": "No",
"successfully_saved": "Successfully saved"
},
"components": {
@ -552,6 +554,14 @@
}
},
"dialogs": {
"voice_command": {
"did_not_hear": "Home Assistant did not hear anything",
"found": "I found the following for you:",
"error": "Oops, an error has occurred",
"how_can_i_help": "How can I help?",
"label": "Type a question and press <Enter>",
"label_voice": "Type and press <Enter> or tap the microphone icon to speak"
},
"confirmation": {
"cancel": "Cancel",
"ok": "OK",
@ -610,6 +620,9 @@
"area_picker_label": "Area",
"update_name_button": "Update Name"
}
},
"domain_toggler": {
"title": "Toggle Domains"
}
},
"duration": {
@ -726,7 +739,19 @@
"picker": {
"header": "Customization",
"introduction": "Tweak per-entity attributes. Added/edited customizations will take effect immediately. Removed customizations will take effect when the entity is updated."
}
},
"warning": {
"include_sentence": "It seems that your configuration.yaml doesn't properly",
"include_link": "include customize.yaml",
"not_applied": "Changes made here are written in it, but will not be applied after a configuration reload unless the include is in place."
},
"attributes_customize": "The following attributes are already set in customize.yaml",
"attributes_outside": "The following attributes are customized from outside of customize.yaml",
"different_include": "Possibly via a domain, a glob or a different include.",
"attributes_set": "The following attributes of the entity are set programmatically.",
"attributes_override": "You can override them if you like.",
"attributes_not_set": "The following attributes weren't set. Set them if you like.",
"pick_attribute": "Pick an attribute to override"
},
"automation": {
"caption": "Automation",
@ -737,7 +762,12 @@
"learn_more": "Learn more about automations",
"pick_automation": "Pick automation to edit",
"no_automations": "We couldnt find any editable automations",
"add_automation": "Add automation"
"add_automation": "Add automation",
"only_editable": "Only automations defined in automations.yaml are editable.",
"edit_automation": "Edit automation",
"show_info_automation": "Show info about automation",
"delete_automation": "Delete automation",
"delete_confirm": "Are you sure you want to delete this automation?"
},
"editor": {
"introduction": "Use automations to bring your home alive.",
@ -954,13 +984,61 @@
"introduction": "The script editor allows you to create and edit scripts. Please follow the link below to read the instructions to make sure that you have configured Home Assistant correctly.",
"learn_more": "Learn more about scripts",
"no_scripts": "We couldnt find any editable scripts",
"add_script": "Add script"
"add_script": "Add script",
"trigger_script": "Trigger script",
"edit_script": "Edit script"
},
"editor": {
"introduction": "Use scripts to execute a sequence of actions.",
"header": "Script: {name}",
"default_name": "New Script",
"load_error_not_editable": "Only scripts inside scripts.yaml are editable.",
"delete_confirm": "Are you sure you want to delete this script?"
"delete_confirm": "Are you sure you want to delete this script?",
"delete_script": "Delete script",
"sequence": "Sequence",
"sequence_sentence": "The sequence of actions of this script.",
"link_available_actions": "Learn more about available actions."
}
},
"scene": {
"caption": "Scenes",
"description": "Create and edit scenes",
"activated": "Activated scene {name}.",
"picker": {
"header": "Scene Editor",
"introduction": "The scene editor allows you to create and edit scenes. Please follow the link below to read the instructions to make sure that you have configured Home Assistant correctly.",
"learn_more": "Learn more about scenes",
"pick_scene": "Pick scene to edit",
"no_scenes": "We couldnt find any editable scenes",
"add_scene": "Add scene",
"only_editable": "Only scenes defined in scenes.yaml are editable.",
"edit_scene": "Edit scene",
"show_info_scene": "Show info about scene",
"delete_scene": "Delete scene",
"delete_confirm": "Are you sure you want to delete this scene?"
},
"editor": {
"introduction": "Use scenes to bring your home alive.",
"default_name": "New Scene",
"load_error_not_editable": "Only scenes in scenes.yaml are editable.",
"load_error_unknown": "Error loading scene ({err_no}).",
"save": "Save",
"unsaved_confirm": "You have unsaved changes. Are you sure you want to leave?",
"name": "Name",
"devices": {
"header": "Devices",
"introduction": "Add the devices that you want to be included in your scene. Set all the devices to the state you want for this scene.",
"add": "Add a device",
"delete": "Delete device"
},
"entities": {
"header": "Entities",
"introduction": "Entities that do not belong to a devices can be set here.",
"without_device": "Entities without device",
"device_entities": "If you add an entity that belongs to a device, the device will be added.",
"add": "Add an entity",
"delete": "Delete entity"
}
}
},
"cloud": {
@ -1121,6 +1199,9 @@
"devices": {
"caption": "Devices",
"description": "Manage connected devices",
"unnamed_device": "Unnamed device",
"unknown_error": "Unknown error",
"area_picker_label": "Area",
"automation": {
"triggers": {
"caption": "Do something when..."
@ -1131,6 +1212,20 @@
"actions": {
"caption": "When something is triggered..."
}
},
"device_not_found": "Device not found.",
"info": "Device info",
"details": "Here are all the details of your device.",
"entities": "Entities",
"automations": "Automations",
"confirm_rename_entity_ids": "Do you also want to rename the entity id's of your entities?",
"data_table": {
"device": "Device",
"manufacturer": "Manufacturer",
"model": "Model",
"area": "Area",
"integration": "Integration",
"battery": "Battery"
}
},
"entity_registry": {
@ -1158,7 +1253,8 @@
"delete": "DELETE",
"confirm_delete": "Are you sure you want to delete this entry?",
"confirm_delete2": "Deleting an entry will not remove the entity from Home Assistant. To do this, you will need to remove the integration '{platform}' from Home Assistant.",
"update": "UPDATE"
"update": "UPDATE",
"note": "Note: this might not work yet with all integrations."
}
},
"person": {
@ -1198,6 +1294,7 @@
"home_assistant_website": "Home Assistant website",
"configure": "Configure",
"none": "Nothing configured yet",
"integration_not_found": "Integration not found.",
"config_entry": {
"settings_button": "Edit settings for {integration}",
"system_options_button": "System options for {integration}",
@ -1215,6 +1312,17 @@
"no_area": "No Area"
},
"config_flow": {
"aborted": "Aborted",
"close": "Close",
"finish": "Finish",
"submit": "Submit",
"not_all_required_fields": "Not all required fields are filled in.",
"add_area": "Add Area",
"area_picker_label": "Area",
"failed_create_area": "Failed to create area.",
"error_saving_area": "Error saving area: {error}",
"name_new_area": "Name of the new area?",
"created_config": "Created config for {name}.",
"external_step": {
"description": "This step requires you to visit an external website to be completed.",
"open_site": "Open website"
@ -1363,7 +1471,8 @@
},
"logbook": {
"showing_entries": "[%key:ui::panel::history::showing_entries%]",
"period": "Period"
"period": "Period",
"entries_not_found": "No logbook entries found."
},
"lovelace": {
"cards": {
@ -1388,15 +1497,24 @@
"more_info": "Show more-info: {name}"
}
},
"unused_entities": {
"title": "Unused entities",
"available_entities": "These are the entities that you have available, but are not in your Lovelace UI yet.",
"select_to_add": "Select the entities you want to add to a card and then click the add card button.",
"entity": "Entity",
"entity_id": "Entity ID",
"domain": "Domain",
"last_changed": "Last Changed"
},
"views": {
"confirm_delete": "Are you sure you want to delete this view?",
"existing_cards": "You can't delete a view that has cards in it. Remove the cards first."
},
"menu": {
"configure_ui": "Configure UI",
"unused_entities": "Unused entities",
"help": "Help",
"refresh": "Refresh"
"refresh": "Refresh",
"close": "Close"
},
"editor": {
"header": "Edit UI",
@ -1408,18 +1526,26 @@
"header": "Edit Config",
"save": "Save",
"unsaved_changes": "Unsaved changes",
"saved": "Saved"
"saved": "Saved",
"confirm_unsaved_changes": "You have unsaved changes, are you sure you want to exit?",
"confirm_unsaved_comments": "Your config contains comment(s), these will not be saved. Do you want to continue?",
"error_parse_yaml": "Unable to parse YAML: {error}",
"error_invalid_config": "Your config is not valid: {error}",
"error_save_yaml": "Unable to save YAML: {error}"
},
"edit_lovelace": {
"header": "Title of your Lovelace UI",
"explanation": "This title is shown above all your views in Lovelace."
"explanation": "This title is shown above all your views in Lovelace.",
"edit_title": "Edit title"
},
"edit_view": {
"header": "View Configuration",
"header_name": "{name} View Configuration",
"add": "Add view",
"edit": "Edit view",
"delete": "Delete view"
"delete": "Delete view",
"move_left": "Move view left",
"move_right": "Move view right"
},
"edit_card": {
"header": "Card Configuration",
@ -1430,8 +1556,8 @@
"show_code_editor": "Show Code Editor",
"add": "Add Card",
"edit": "Edit",
"delete": "Delete",
"move": "Move",
"delete": "Delete Card",
"move": "Move to View",
"options": "More options"
},
"save_config": {
@ -1643,7 +1769,9 @@
},
"advanced_mode": {
"title": "Advanced Mode",
"description": "Home Assistant hides advanced features and options by default. You can make these features accessible by checking this toggle. This is a user-specific setting and does not impact other users using Home Assistant."
"description": "Home Assistant hides advanced features and options by default. You can make these features accessible by checking this toggle. This is a user-specific setting and does not impact other users using Home Assistant.",
"hint_enable": "Missing config options? Enable advanced mode on",
"link_profile_page": "your profile page"
},
"refresh_tokens": {
"header": "Refresh Tokens",
@ -1675,7 +1803,7 @@
"shopping-list": {
"clear_completed": "Clear completed",
"add_item": "Add item",
"microphone_tip": "Tap the microphone on the top right and say “Add candy to my shopping list”"
"microphone_tip": "Tap the microphone on the top right and say or type “Add candy to my shopping list”"
},
"page-authorize": {
"initializing": "Initializing",
@ -1952,6 +2080,13 @@
"more_integrations": "More",
"finish": "Finish"
}
},
"custom": {
"external_panel": {
"question_trust": "Do you trust the external panel {name} at {link}?",
"complete_access": "It will have access to all data in Home Assistant.",
"hide_message": "Check docs for the panel_custom component to hide this message"
}
}
}
}

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