mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-01 13:37:47 +00:00
commit
bc01df42d8
@ -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/",
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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",
|
||||
|
2
setup.py
2
setup.py
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
);
|
||||
|
||||
|
@ -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
16
src/data/conversation.ts
Normal 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 });
|
@ -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,
|
||||
|
@ -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
91
src/data/scene.ts
Normal 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}`);
|
@ -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>
|
||||
`;
|
||||
},
|
||||
});
|
||||
|
@ -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>
|
||||
`;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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``}
|
||||
|
@ -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>
|
||||
|
@ -3,6 +3,8 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
export interface ConfirmationDialogParams {
|
||||
title?: string;
|
||||
text: string;
|
||||
confirmBtnText?: string;
|
||||
cancelBtnText?: string;
|
||||
confirm: () => void;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) =>
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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);
|
@ -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";
|
||||
|
@ -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";
|
||||
|
455
src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts
Normal file
455
src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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: {},
|
||||
});
|
||||
};
|
@ -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(
|
||||
|
@ -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) {
|
||||
|
@ -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() {
|
||||
|
@ -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!);
|
||||
|
@ -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>
|
||||
`
|
||||
: ""
|
||||
|
@ -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";
|
||||
|
@ -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=""
|
||||
>
|
||||
|
@ -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>
|
||||
|
@ -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 },
|
||||
|
@ -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"
|
||||
)})`;
|
||||
|
@ -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) => {
|
||||
|
@ -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}`);
|
||||
|
@ -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>
|
||||
|
@ -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: () =>
|
||||
|
@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ export default class ZoneCondition extends Component<any> {
|
||||
onChange={this.zonePicked}
|
||||
hass={hass}
|
||||
allowCustomEntity
|
||||
domainFilter="zone"
|
||||
includeDomains={["zone"]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -24,7 +24,7 @@ export default class SceneAction extends Component<any> {
|
||||
value={scene}
|
||||
onChange={this.sceneChanged}
|
||||
hass={hass}
|
||||
domainFilter="scene"
|
||||
includeDomains={["scene"]}
|
||||
allowCustomEntity
|
||||
/>
|
||||
</div>
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
81
src/panels/config/scene/ha-config-scene.ts
Normal file
81
src/panels/config/scene/ha-config-scene.ts
Normal 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;
|
||||
}
|
||||
}
|
213
src/panels/config/scene/ha-scene-dashboard.ts
Normal file
213
src/panels/config/scene/ha-scene-dashboard.ts
Normal 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;
|
||||
}
|
||||
}
|
738
src/panels/config/scene/ha-scene-editor.ts
Normal file
738
src/panels/config/scene/ha-scene-editor.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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";
|
||||
|
||||
/*
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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() {
|
||||
|
@ -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]]">
|
||||
|
@ -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!);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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!);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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!);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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!);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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!);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
);
|
@ -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);
|
||||
};
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 ||
|
@ -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";
|
||||
}
|
@ -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);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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!);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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!);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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!);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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()
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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 couldn’t 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 couldn’t 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 couldn’t 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
Loading…
x
Reference in New Issue
Block a user