Merge pull request #2190 from home-assistant/dev

20181205.0
This commit is contained in:
Paulus Schoutsen 2018-12-05 22:40:18 +01:00 committed by GitHub
commit f5022f4e1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
184 changed files with 6477 additions and 2541 deletions

View File

@ -69,7 +69,7 @@ class DemoAlarmPanelEntity extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -74,7 +74,7 @@ class DemoConditional extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -188,7 +188,7 @@ class DemoEntities extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -89,7 +89,7 @@ class DemoEntityButtonEntity extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -105,7 +105,7 @@ class DemoFilter extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -89,10 +89,10 @@ const CONFIGS = [
`,
},
{
heading: "Custom column width",
heading: "Custom number of columns",
config: `
- type: glance
column_width: calc(100% / 7)
columns: 7
entities:
- device_tracker.demo_paulus
- media_player.living_room
@ -230,7 +230,7 @@ class DemoPicEntity extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -38,7 +38,7 @@ class DemoLightEntity extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -139,7 +139,7 @@ class DemoMap extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -95,7 +95,7 @@ class DemoHuiMediaPlayerRows extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -93,7 +93,7 @@ class DemoPicElements extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -36,7 +36,7 @@ class DemoShoppingListEntity extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);

View File

@ -104,7 +104,7 @@ class DemoStack extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -75,7 +75,7 @@ class DemoThermostatEntity extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);

View File

@ -8,16 +8,7 @@ import getEntity from "../data/entity";
import provideHass from "../data/provide_hass";
import "../components/demo-more-infos";
/* eslint-disable no-unused-vars */
const SUPPORT_BRIGHTNESS = 1;
const SUPPORT_COLOR_TEMP = 2;
const SUPPORT_EFFECT = 4;
const SUPPORT_FLASH = 8;
const SUPPORT_COLOR = 16;
const SUPPORT_TRANSITION = 32;
const SUPPORT_WHITE_VALUE = 128;
import { SUPPORT_BRIGHTNESS } from "../../../src/data/light";
const ENTITIES = [
getEntity("light", "bed_light", "on", {
@ -49,7 +40,7 @@ class DemoMoreInfoLight extends PolymerElement {
};
}
ready() {
public ready() {
super.ready();
const hass = provideHass(this);
hass.addEntities(ENTITIES);

View File

@ -0,0 +1,79 @@
import { html, LitElement } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-button/paper-button";
import "../../../src/components/ha-card";
import { longPress } from "../../../src/panels/lovelace/common/directives/long-press-directive";
export class DemoUtilLongPress extends LitElement {
public render(): TemplateResult {
return html`
${this.renderStyle()}
${
[1, 2, 3].map(
() => html`
<ha-card>
<paper-button
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
>
(long) press me!
</paper-button>
<textarea></textarea>
<div>(try pressing and scrolling too!)</div>
</ha-card>
`
)
}
`;
}
private _handleTap(ev: Event) {
this._addValue(ev, "tap");
}
private _handleHold(ev: Event) {
this._addValue(ev, "hold");
}
private _addValue(ev: Event, value: string) {
const area = (ev.currentTarget as HTMLElement)
.nextElementSibling! as HTMLTextAreaElement;
const now = new Date().toTimeString().split(" ")[0];
area.value += `${now}: ${value}\n`;
area.scrollTop = area.scrollHeight;
}
private renderStyle() {
return html`
<style>
ha-card {
width: 200px;
margin: calc(42vh - 140px) auto;
padding: 8px;
text-align: center;
}
ha-card:first-of-type {
margin-top: 16px;
}
ha-card:last-of-type {
margin-bottom: 16px;
}
paper-button {
font-weight: bold;
color: var(--primary-color);
}
textarea {
height: 50px;
}
</style>
`;
}
}
customElements.define("demo-util-long-press", DemoUtilLongPress);

View File

@ -11,7 +11,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../src/managers/notification-manager";
const DEMOS = require.context("./demos", true, /^(.*\.(js$))[^.]*$/im);
const DEMOS = require.context("./demos", true, /^(.*\.(ts$))[^.]*$/im);
const fixPath = (path) => path.substr(2, path.length - 5);
@ -118,6 +118,22 @@ class HaGallery extends PolymerElement {
</a>
</template>
</paper-card>
<paper-card heading="Util demos">
<div class='card-content intro'>
<p>
Test pages for our utility functions.
</p>
</div>
<template is='dom-repeat' items='[[_utilDemos]]'>
<a href='#[[item]]'>
<paper-item>
<paper-item-body>{{ item }}</paper-item-body>
<iron-icon icon="hass:chevron-right"></iron-icon>
</paper-item>
</a>
</template>
</paper-card>
</div>
</template>
</div>
@ -145,6 +161,10 @@ class HaGallery extends PolymerElement {
type: Array,
computed: "_computeMoreInfos(_demos)",
},
_utilDemos: {
type: Array,
computed: "_computeUtil(_demos)",
},
};
}
@ -178,7 +198,7 @@ class HaGallery extends PolymerElement {
while (root.lastChild) root.removeChild(root.lastChild);
if (demo) {
DEMOS(`./${demo}.js`);
DEMOS(`./${demo}.ts`);
const el = document.createElement(demo);
root.appendChild(el);
}
@ -199,6 +219,10 @@ class HaGallery extends PolymerElement {
_computeMoreInfos(demos) {
return demos.filter((demo) => demo.includes("more-info"));
}
_computeUtil(demos) {
return demos.filter((demo) => demo.includes("util"));
}
}
customElements.define("ha-gallery", HaGallery);

View File

@ -2,6 +2,7 @@ import "@polymer/paper-button/paper-button";
import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
import "../../../src/resources/ha-style";
@ -15,10 +16,13 @@ class HassioAddonLogs extends PolymerElement {
}
pre {
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: break-word;
}
</style>
${ANSI_HTML_STYLE}
<paper-card heading="Log">
<div class="card-content"><pre>[[log]]</pre></div>
<div class="card-content" id="content"></div>
<div class="card-actions">
<paper-button on-click="refresh">Refresh</paper-button>
</div>
@ -33,7 +37,6 @@ class HassioAddonLogs extends PolymerElement {
type: String,
observer: "addonSlugChanged",
},
log: String,
};
}
@ -51,8 +54,11 @@ class HassioAddonLogs extends PolymerElement {
refresh() {
this.hass
.callApi("get", `hassio/addons/${this.addonSlug}/logs`)
.then((info) => {
this.log = info;
.then((text) => {
while (this.$.content.lastChild) {
this.$.content.removeChild(this.$.content.lastChild);
}
this.$.content.appendChild(parseTextToColoredPre(text));
});
}
}

203
hassio/src/ansi-to-html.js Normal file
View File

@ -0,0 +1,203 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
export const ANSI_HTML_STYLE = html`
<style>
.bold {
font-weight: bold;
}
.italic {
font-style: italic;
}
.underline {
text-decoration: underline;
}
.strikethrough {
text-decoration: line-through;
}
.underline.strikethrough {
text-decoration: underline line-through;
}
.fg-red {
color: rgb(222, 56, 43);
}
.fg-green {
color: rgb(57, 181, 74);
}
.fg-yellow {
color: rgb(255, 199, 6);
}
.fg-blue {
color: rgb(0, 111, 184);
}
.fg-magenta {
color: rgb(118, 38, 113);
}
.fg-cyan {
color: rgb(44, 181, 233);
}
.fg-white {
color: rgb(204, 204, 204);
}
.bg-black {
background-color: rgb(0, 0, 0);
}
.bg-red {
background-color: rgb(222, 56, 43);
}
.bg-green {
background-color: rgb(57, 181, 74);
}
.bg-yellow {
background-color: rgb(255, 199, 6);
}
.bg-blue {
background-color: rgb(0, 111, 184);
}
.bg-magenta {
background-color: rgb(118, 38, 113);
}
.bg-cyan {
background-color: rgb(44, 181, 233);
}
.bg-white {
background-color: rgb(204, 204, 204);
}
</style>
`;
export function parseTextToColoredPre(text) {
const pre = document.createElement("pre");
const re = /\033(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
let i = 0;
const state = {
bold: false,
italic: false,
underline: false,
strikethrough: false,
foregroundColor: null,
backgroundColor: null,
};
const addSpan = (content) => {
const span = document.createElement("span");
if (state.bold) span.classList.add("bold");
if (state.italic) span.classList.add("italic");
if (state.underline) span.classList.add("underline");
if (state.strikethrough) span.classList.add("strikethrough");
if (state.foregroundColor !== null)
span.classList.add(`fg-${state.foregroundColor}`);
if (state.backgroundColor !== null)
span.classList.add(`bg-${state.backgroundColor}`);
span.appendChild(document.createTextNode(content));
pre.appendChild(span);
};
/* eslint-disable no-cond-assign */
let match;
while ((match = re.exec(text)) !== null) {
const j = match.index;
addSpan(text.substring(i, j));
i = j + match[0].length;
if (match[1] === undefined) continue;
for (const colorCode of match[1].split(";")) {
switch (parseInt(colorCode)) {
case 0:
// reset
state.bold = false;
state.italic = false;
state.underline = false;
state.strikethrough = false;
state.foregroundColor = null;
state.backgroundColor = null;
break;
case 1:
state.bold = true;
break;
case 3:
state.italic = true;
break;
case 4:
state.underline = true;
break;
case 9:
state.strikethrough = true;
break;
case 22:
state.bold = false;
break;
case 23:
state.italic = false;
break;
case 24:
state.underline = false;
break;
case 29:
state.strikethrough = false;
break;
case 30:
// foreground black
state.foregroundColor = null;
break;
case 31:
state.foregroundColor = "red";
break;
case 32:
state.foregroundColor = "green";
break;
case 33:
state.foregroundColor = "yellow";
break;
case 34:
state.foregroundColor = "blue";
break;
case 35:
state.foregroundColor = "magenta";
break;
case 36:
state.foregroundColor = "cyan";
break;
case 37:
state.foregroundColor = "white";
break;
case 39:
// foreground reset
state.foregroundColor = null;
break;
case 40:
state.backgroundColor = "black";
break;
case 41:
state.backgroundColor = "red";
break;
case 42:
state.backgroundColor = "green";
break;
case 43:
state.backgroundColor = "yellow";
break;
case 44:
state.backgroundColor = "blue";
break;
case 45:
state.backgroundColor = "magenta";
break;
case 46:
state.backgroundColor = "cyan";
break;
case 47:
state.backgroundColor = "white";
break;
case 49:
// background reset
state.backgroundColor = null;
break;
}
}
}
addSpan(text.substring(i));
return pre;
}

View File

@ -2,6 +2,7 @@ import "@polymer/paper-button/paper-button";
import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
class HassioSupervisorLog extends PolymerElement {
static get template() {
@ -12,12 +13,18 @@ class HassioSupervisorLog extends PolymerElement {
}
pre {
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: break-word;
}
.fg-green {
color: var(--primary-text-color) !important;
}
</style>
${ANSI_HTML_STYLE}
<paper-card>
<div class="card-content"><pre>[[log]]</pre></div>
<div class="card-content" id="content"></div>
<div class="card-actions">
<paper-button on-click="refreshTapped">Refresh</paper-button>
<paper-button on-click="refresh">Refresh</paper-button>
</div>
</paper-card>
`;
@ -26,7 +33,6 @@ class HassioSupervisorLog extends PolymerElement {
static get properties() {
return {
hass: Object,
log: String,
};
}
@ -37,16 +43,20 @@ class HassioSupervisorLog extends PolymerElement {
loadData() {
this.hass.callApi("get", "hassio/supervisor/logs").then(
(info) => {
this.log = info;
(text) => {
while (this.$.content.lastChild) {
this.$.content.removeChild(this.$.content.lastChild);
}
this.$.content.appendChild(parseTextToColoredPre(text));
},
() => {
this.log = "Error fetching logs";
this.$.content.innerHTML =
'<span class="fg-red bold">Error fetching logs</span>';
}
);
}
refreshTapped() {
refresh() {
this.loadData();
}
}

View File

@ -64,10 +64,10 @@
"@polymer/paper-toggle-button": "^3.0.1",
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "^3.0.5",
"@vaadin/vaadin-combo-box": "^4.2.0-beta2",
"@vaadin/vaadin-date-picker": "^3.3.0",
"@webcomponents/shadycss": "^1.5.2",
"@webcomponents/webcomponentsjs": "^2.1.3",
"@vaadin/vaadin-combo-box": "^4.2.0",
"@vaadin/vaadin-date-picker": "^3.3.1",
"@webcomponents/shadycss": "^1.6.0",
"@webcomponents/webcomponentsjs": "^2.2.0",
"chart.js": "~2.7.2",
"chartjs-chart-timeline": "^0.2.1",
"es6-object-assign": "^1.1.0",
@ -87,6 +87,7 @@
"react-big-calendar": "^0.19.2",
"regenerator-runtime": "^0.12.1",
"round-slider": "^1.3.2",
"superstruct": "^0.6.0",
"unfetch": "^4.0.1",
"web-animations-js": "^2.3.1",
"xss": "^1.0.3"
@ -152,15 +153,11 @@
"workbox-webpack-plugin": "^3.5.0"
},
"resolutions": {
"inherits": "2.0.3",
"samsam": "1.1.3",
"supports-color": "3.1.2",
"type-detect": "1.0.0",
"@polymer/polymer": "3.1.0",
"@webcomponents/webcomponentsjs": "2.1.3",
"@webcomponents/shadycss": "^1.5.2",
"@vaadin/vaadin-overlay": "3.2.0-alpha3",
"@vaadin/vaadin-lumo-styles": "1.2.0",
"@webcomponents/webcomponentsjs": "2.2.1",
"@webcomponents/shadycss": "^1.6.0",
"@vaadin/vaadin-overlay": "3.2.2",
"@vaadin/vaadin-lumo-styles": "1.3.0",
"fecha": "https://github.com/taylorhakes/fecha/archive/5e8fe08d982647fdb19fb403459838b02647813c.tar.gz",
"lit-html": "0.12.0",
"@polymer/lit-element": "0.6.2"

View File

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

View File

@ -103,6 +103,7 @@ class HaPlantCard extends EventsMixin(PolymerElement) {
return {
hass: Object,
stateObj: Object,
config: Object,
};
}
@ -118,7 +119,7 @@ class HaPlantCard extends EventsMixin(PolymerElement) {
}
computeTitle(stateObj) {
return computeStateName(stateObj);
return this.config.name || computeStateName(stateObj);
}
computeAttributes(data) {

View File

@ -1,6 +1,8 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import computeStateName from "../common/entity/compute_state_name";
import "../components/ha-card";
import "../components/ha-icon";
@ -106,7 +108,7 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
<ha-card>
<div class="header">
[[computeState(stateObj.state, localize)]]
<div class="name">[[stateObj.attributes.friendly_name]]</div>
<div class="name">[[computeName(stateObj)]]</div>
</div>
<div class="content">
<div class="now">
@ -271,6 +273,10 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
return localize(`state.weather.${state}`) || state;
}
computeName(stateObj) {
return this.config.name || computeStateName(stateObj);
}
showWeatherIcon(condition) {
return condition in this.weatherIcons;
}

View File

@ -10,31 +10,43 @@ const langKey = ["second", "minute", "hour", "day"];
export default function relativeTime(
dateObj: Date,
localize: LocalizeFunc
localize: LocalizeFunc,
options: {
compareTime?: Date;
includeTense?: boolean;
} = {}
): string {
let delta = (new Date().getTime() - dateObj.getTime()) / 1000;
const compareTime = options.compareTime || new Date();
let delta = (compareTime.getTime() - dateObj.getTime()) / 1000;
const tense = delta >= 0 ? "past" : "future";
delta = Math.abs(delta);
let timeDesc;
for (let i = 0; i < tests.length; i++) {
if (delta < tests[i]) {
delta = Math.floor(delta);
const timeDesc = localize(
timeDesc = localize(
`ui.components.relative_time.duration.${langKey[i]}`,
"count",
delta
);
return localize(`ui.components.relative_time.${tense}`, "time", timeDesc);
break;
}
delta /= tests[i];
}
delta = Math.floor(delta);
const time = localize(
"ui.components.relative_time.duration.week",
"count",
delta
);
return localize(`ui.components.relative_time.${tense}`, "time", time);
if (timeDesc === undefined) {
delta = Math.floor(delta);
timeDesc = localize(
"ui.components.relative_time.duration.week",
"count",
delta
);
}
return options.includeTense === false
? timeDesc
: localize(`ui.components.relative_time.${tense}`, "time", timeDesc);
}

View File

@ -28,6 +28,17 @@
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
declare global {
// tslint:disable-next-line
interface HASSDomEvents {}
}
export type ValidHassDomEvent = keyof HASSDomEvents;
export interface HASSDomEvent<T> extends Event {
detail: T;
}
/**
* Dispatches a custom event with an optional detail value.
*
@ -35,23 +46,33 @@
* @param {*=} detail Detail value containing event-specific
* payload.
* @param {{ bubbles: (boolean|undefined),
cancelable: (boolean|undefined),
composed: (boolean|undefined) }=}
* options Object specifying options. These may include:
* `bubbles` (boolean, defaults to `true`),
* `cancelable` (boolean, defaults to false), and
* `node` on which to fire the event (HTMLElement, defaults to `this`).
* @return {Event} The new event that was fired.
*/
export const fireEvent = (node, type, detail, options) => {
* cancelable: (boolean|undefined),
* composed: (boolean|undefined) }=}
* options Object specifying options. These may include:
* `bubbles` (boolean, defaults to `true`),
* `cancelable` (boolean, defaults to false), and
* `node` on which to fire the event (HTMLElement, defaults to `this`).
* @return {Event} The new event that was fired.
*/
export const fireEvent = <HassEvent extends ValidHassDomEvent>(
node: HTMLElement,
type: HassEvent,
detail?: HASSDomEvents[HassEvent],
options?: {
bubbles?: boolean;
cancelable?: boolean;
composed?: boolean;
}
) => {
options = options || {};
// @ts-ignore
detail = detail === null || detail === undefined ? {} : detail;
const event = new Event(type, {
bubbles: options.bubbles === undefined ? true : options.bubbles,
cancelable: Boolean(options.cancelable),
composed: options.composed === undefined ? true : options.composed,
});
event.detail = detail;
(event as any).detail = detail;
node.dispatchEvent(event);
return event;
};

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import { GroupEntity } from "../../types";
export default function getViewEntities(
entities: HassEntities,
view: GroupEntity
) {
): HassEntities {
const viewEntities = {};
view.attributes.entity_id.forEach((entityId) => {

View File

@ -1,18 +1,19 @@
import computeDomain from "./compute_domain";
import { HassEntity, HassEntities } from "home-assistant-js-websocket";
import { HassEntities } from "home-assistant-js-websocket";
import { GroupEntity } from "../../types";
// Split a collection into a list of groups and a 'rest' list of ungrouped
// entities.
// Returns { groups: [], ungrouped: {} }
export default function splitByGroups(entities: HassEntities) {
const groups: HassEntity[] = [];
const groups: GroupEntity[] = [];
const ungrouped: HassEntities = {};
Object.keys(entities).forEach((entityId) => {
const entity = entities[entityId];
if (computeDomain(entityId) === "group") {
groups.push(entity);
groups.push(entity as GroupEntity);
} else {
ungrouped[entityId] = entity;
}

View File

@ -0,0 +1,9 @@
import { HassEntity } from "home-assistant-js-websocket";
export const supportsFeature = (
stateObj: HassEntity,
feature: number
): boolean => {
// tslint:disable-next-line:no-bitwise
return (stateObj.attributes.supported_features! & feature) !== 0;
};

View File

@ -0,0 +1,9 @@
import { HomeAssistant } from "../../types";
export function computeRTL(hass: HomeAssistant) {
const lang = hass.language || "en";
if (hass.translationMetadata.translations[lang]) {
return hass.translationMetadata.translations[lang].isRTL || false;
}
return false;
}

9
src/common/util/uid.ts Normal file
View File

@ -0,0 +1,9 @@
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
export function uid() {
return s4() + s4() + s4() + s4() + s4();
}

View File

@ -4,6 +4,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../ha-relative-time";
import "./state-badge";
import computeStateName from "../../common/entity/compute_state_name";
import { computeRTL } from "../../common/util/compute_rtl";
class StateInfo extends PolymerElement {
static get template() {
@ -25,10 +26,20 @@ class StateInfo extends PolymerElement {
float: left;
}
:host([rtl]) state-badge {
float: right;
}
.info {
margin-left: 56px;
}
:host([rtl]) .info {
margin-right: 56px;
margin-left: 0;
text-align: right;
}
.name {
@apply --paper-font-common-nowrap;
color: var(--primary-text-color);
@ -87,12 +98,21 @@ class StateInfo extends PolymerElement {
hass: Object,
stateObj: Object,
inDialog: Boolean,
rtl: {
type: Boolean,
reflectToAttribute: true,
computed: "computeRTL(hass)",
},
};
}
computeStateName(stateObj) {
return computeStateName(stateObj);
}
computeRTL(hass) {
return computeRTL(hass);
}
}
customElements.define("state-info", StateInfo);

View File

@ -12,6 +12,7 @@ class HaCard extends PolymerElement {
border-radius: 2px;
transition: all 0.3s ease-out;
background-color: var(--paper-card-background-color, white);
color: var(--primary-text-color);
}
.header {
@apply --paper-font-headline;

View File

@ -123,7 +123,7 @@ class HaCards extends PolymerElement {
</style>
<div id="main">
<template is="dom-if" if="[[cards.badges]]">
<template is="dom-if" if="[[cards.badges.length]]">
<div class="badges">
<template is="dom-if" if="[[cards.demo]]">
<ha-demo-badge></ha-demo-badge>
@ -159,7 +159,6 @@ class HaCards extends PolymerElement {
},
states: Object,
panelVisible: Boolean,
viewVisible: {
type: Boolean,
@ -173,19 +172,11 @@ class HaCards extends PolymerElement {
}
static get observers() {
return [
"updateCards(columns, states, panelVisible, viewVisible, orderedGroupEntities)",
];
return ["updateCards(columns, states, viewVisible, orderedGroupEntities)"];
}
updateCards(
columns,
states,
panelVisible,
viewVisible,
orderedGroupEntities
) {
if (!panelVisible || !viewVisible) {
updateCards(columns, states, viewVisible, orderedGroupEntities) {
if (!viewVisible) {
if (this.$.main.parentNode) {
this.$.main._parentNode = this.$.main.parentNode;
this.$.main.parentNode.removeChild(this.$.main);
@ -200,7 +191,7 @@ class HaCards extends PolymerElement {
timeOut.after(10),
() => {
// Things might have changed since it got scheduled.
if (this.panelVisible && this.viewVisible) {
if (this.viewVisible) {
this.cards = this.computeCards(columns, states, orderedGroupEntities);
}
}

19
src/data/cloud.ts Normal file
View File

@ -0,0 +1,19 @@
import { HomeAssistant } from "../types";
export interface CloudWebhook {
webhook_id: string;
cloudhook_id: string;
cloudhook_url: string;
}
export const createCloudhook = (hass: HomeAssistant, webhookId: string) =>
hass.callWS<CloudWebhook>({
type: "cloud/cloudhook/create",
webhook_id: webhookId,
});
export const deleteCloudhook = (hass: HomeAssistant, webhookId: string) =>
hass.callWS({
type: "cloud/cloudhook/delete",
webhook_id: webhookId,
});

1
src/data/entity.ts Normal file
View File

@ -0,0 +1 @@
export const UNAVAILABLE = "unavailable";

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

@ -0,0 +1,7 @@
export const SUPPORT_BRIGHTNESS = 1;
export const SUPPORT_COLOR_TEMP = 2;
export const SUPPORT_EFFECT = 4;
export const SUPPORT_FLASH = 8;
export const SUPPORT_COLOR = 16;
export const SUPPORT_TRANSITION = 32;
export const SUPPORT_WHITE_VALUE = 128;

150
src/data/lovelace.ts Normal file
View File

@ -0,0 +1,150 @@
import { HomeAssistant } from "../types";
export interface LovelaceConfig {
_frontendAuto: boolean;
title?: string;
views: LovelaceViewConfig[];
}
export interface LovelaceViewConfig {
title?: string;
badges?: string[];
cards?: LovelaceCardConfig[];
id?: string;
icon?: string;
theme?: string;
}
export interface LovelaceCardConfig {
id?: string;
type: string;
[key: string]: any;
}
export interface ToggleActionConfig {
action: "toggle";
}
export interface CallServiceActionConfig {
action: "call-service";
service: string;
service_data?: { [key: string]: any };
}
export interface NavigateActionConfig {
action: "navigate";
navigation_path: string;
}
export interface MoreInfoActionConfig {
action: "more-info";
}
export interface NoActionConfig {
action: "none";
}
export type ActionConfig =
| ToggleActionConfig
| CallServiceActionConfig
| NavigateActionConfig
| MoreInfoActionConfig
| NoActionConfig;
export const fetchConfig = (hass: HomeAssistant): Promise<LovelaceConfig> =>
hass.callWS({
type: "lovelace/config",
});
export const migrateConfig = (hass: HomeAssistant): Promise<void> =>
hass.callWS({
type: "lovelace/config/migrate",
});
export const saveConfig = (
hass: HomeAssistant,
config: LovelaceConfig | string,
format: "json" | "yaml"
): Promise<void> =>
hass.callWS({
type: "lovelace/config/save",
config,
format,
});
export const getCardConfig = (
hass: HomeAssistant,
cardId: string
): Promise<string> =>
hass.callWS({
type: "lovelace/config/card/get",
card_id: cardId,
});
export const updateCardConfig = (
hass: HomeAssistant,
cardId: string,
config: LovelaceCardConfig | string,
format: "json" | "yaml"
): Promise<void> =>
hass.callWS({
type: "lovelace/config/card/update",
card_id: cardId,
card_config: config,
format,
});
export const deleteCard = (
hass: HomeAssistant,
cardId: string
): Promise<void> =>
hass.callWS({
type: "lovelace/config/card/delete",
card_id: cardId,
});
export const addCard = (
hass: HomeAssistant,
viewId: string,
config: LovelaceCardConfig | string,
format: "json" | "yaml"
): Promise<void> =>
hass.callWS({
type: "lovelace/config/card/add",
view_id: viewId,
card_config: config,
format,
});
export const updateViewConfig = (
hass: HomeAssistant,
viewId: string,
config: LovelaceViewConfig | string,
format: "json" | "yaml"
): Promise<void> =>
hass.callWS({
type: "lovelace/config/view/update",
view_id: viewId,
view_config: config,
format,
});
export const deleteView = (
hass: HomeAssistant,
viewId: string
): Promise<void> =>
hass.callWS({
type: "lovelace/config/view/delete",
view_id: viewId,
});
export const addView = (
hass: HomeAssistant,
config: LovelaceViewConfig | string,
format: "json" | "yaml"
): Promise<void> =>
hass.callWS({
type: "lovelace/config/view/add",
view_config: config,
format,
});

4
src/data/media-player.ts Normal file
View File

@ -0,0 +1,4 @@
export const SUPPORT_PAUSE = 1;
export const SUPPORT_NEXT_TRACK = 32;
export const SUPPORTS_PLAY = 16384;
export const OFF_STATES = ["off", "idle"];

View File

@ -11,31 +11,30 @@ export const fetchItems = (hass: HomeAssistant): Promise<ShoppingListItem[]> =>
type: "shopping_list/items",
});
export const saveEdit = (
export const updateItem = (
hass: HomeAssistant,
itemId: number,
name: string
item: {
name?: string;
complete?: boolean;
}
): Promise<ShoppingListItem> =>
hass.callApi("POST", "shopping_list/item/" + itemId, {
name,
});
export const completeItem = (
hass: HomeAssistant,
itemId: number,
complete: boolean
): Promise<void> =>
hass.callApi("POST", "shopping_list/item/" + itemId, {
complete,
hass.callWS({
type: "shopping_list/items/update",
item_id: itemId,
...item,
});
export const clearItems = (hass: HomeAssistant): Promise<void> =>
hass.callApi("POST", "shopping_list/clear_completed");
hass.callWS({
type: "shopping_list/items/clear",
});
export const addItem = (
hass: HomeAssistant,
name: string
): Promise<ShoppingListItem> =>
hass.callApi("POST", "shopping_list/item", {
hass.callWS({
type: "shopping_list/items/add",
name,
});

12
src/data/webhook.ts Normal file
View File

@ -0,0 +1,12 @@
import { HomeAssistant } from "../types";
export interface Webhook {
webhook_id: string;
domain: string;
name: string;
}
export const fetchWebhooks = (hass: HomeAssistant): Promise<Webhook[]> =>
hass.callWS({
type: "webhook/list",
});

View File

@ -13,6 +13,7 @@ import "../../../components/ha-paper-slider";
import attributeClassNames from "../../../common/entity/attribute_class_names";
import featureClassNames from "../../../common/entity/feature_class_names";
import { supportsFeature } from "../../../common/entity/supports-feature";
import EventsMixin from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
@ -385,45 +386,45 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
supportsTemperature(stateObj) {
return (
(stateObj.attributes.supported_features & 1) !== 0 &&
supportsFeature(stateObj, 1) &&
typeof stateObj.attributes.temperature === "number"
);
}
supportsTemperatureRange(stateObj) {
return (
(stateObj.attributes.supported_features & 6) !== 0 &&
supportsFeature(stateObj, 6) &&
(typeof stateObj.attributes.target_temp_low === "number" ||
typeof stateObj.attributes.target_temp_high === "number")
);
}
supportsHumidity(stateObj) {
return (stateObj.attributes.supported_features & 8) !== 0;
return supportsFeature(stateObj, 8);
}
supportsFanMode(stateObj) {
return (stateObj.attributes.supported_features & 64) !== 0;
return supportsFeature(stateObj, 64);
}
supportsOperationMode(stateObj) {
return (stateObj.attributes.supported_features & 128) !== 0;
return supportsFeature(stateObj, 128);
}
supportsSwingMode(stateObj) {
return (stateObj.attributes.supported_features & 512) !== 0;
return supportsFeature(stateObj, 512);
}
supportsAwayMode(stateObj) {
return (stateObj.attributes.supported_features & 1024) !== 0;
return supportsFeature(stateObj, 1024);
}
supportsAuxHeat(stateObj) {
return (stateObj.attributes.supported_features & 2048) !== 0;
return supportsFeature(stateObj, 2048);
}
supportsOn(stateObj) {
return (stateObj.attributes.supported_features & 4096) !== 0;
return supportsFeature(stateObj, 4096);
}
computeClassNames(stateObj) {

View File

@ -8,6 +8,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-attributes";
import { supportsFeature } from "../../../common/entity/supports-feature";
class MoreInfoVacuum extends PolymerElement {
static get template() {
@ -158,57 +159,53 @@ class MoreInfoVacuum extends PolymerElement {
};
}
/* eslint-disable no-bitwise */
supportsPause(stateObj) {
return (stateObj.attributes.supported_features & 4) !== 0;
return supportsFeature(stateObj, 4);
}
supportsStop(stateObj) {
return (stateObj.attributes.supported_features & 8) !== 0;
return supportsFeature(stateObj, 8);
}
supportsReturnHome(stateObj) {
return (stateObj.attributes.supported_features & 16) !== 0;
return supportsFeature(stateObj, 16);
}
supportsFanSpeed(stateObj) {
return (stateObj.attributes.supported_features & 32) !== 0;
return supportsFeature(stateObj, 32);
}
supportsBattery(stateObj) {
return (stateObj.attributes.supported_features & 64) !== 0;
return supportsFeature(stateObj, 64);
}
supportsStatus(stateObj) {
return (stateObj.attributes.supported_features & 128) !== 0;
return supportsFeature(stateObj, 128);
}
supportsLocate(stateObj) {
return (stateObj.attributes.supported_features & 512) !== 0;
return supportsFeature(stateObj, 512);
}
supportsCleanSpot(stateObj) {
return (stateObj.attributes.supported_features & 1024) !== 0;
return supportsFeature(stateObj, 1024);
}
supportsStart(stateObj) {
return (stateObj.attributes.supported_features & 8192) !== 0;
return supportsFeature(stateObj, 8192);
}
supportsCommandBar(stateObj) {
return (
((stateObj.attributes.supported_features & 4) !== 0) |
((stateObj.attributes.supported_features & 8) !== 0) |
((stateObj.attributes.supported_features & 16) !== 0) |
((stateObj.attributes.supported_features & 512) !== 0) |
((stateObj.attributes.supported_features & 1024) !== 0) |
((stateObj.attributes.supported_features & 8192) !== 0)
supportsFeature(stateObj, 4) |
supportsFeature(stateObj, 8) |
supportsFeature(stateObj, 16) |
supportsFeature(stateObj, 512) |
supportsFeature(stateObj, 1024) |
supportsFeature(stateObj, 8192)
);
}
/* eslint-enable no-bitwise */
fanSpeedChanged(fanSpeedIndex) {
var fanSpeedInput;
// Selected Option will transition to '' before transitioning to new value

View File

@ -12,6 +12,7 @@ import "../../../components/ha-water_heater-control";
import "../../../components/ha-paper-slider";
import featureClassNames from "../../../common/entity/feature_class_names";
import { supportsFeature } from "../../../common/entity/supports-feature";
import EventsMixin from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
@ -198,17 +199,17 @@ class MoreInfoWaterHeater extends LocalizeMixin(EventsMixin(PolymerElement)) {
supportsTemperature(stateObj) {
return (
(stateObj.attributes.supported_features & 1) !== 0 &&
supportsFeature(stateObj, 1) &&
typeof stateObj.attributes.temperature === "number"
);
}
supportsOperationMode(stateObj) {
return (stateObj.attributes.supported_features & 2) !== 0;
return supportsFeature(stateObj, 2);
}
supportsAwayMode(stateObj) {
return (stateObj.attributes.supported_features & 4) !== 0;
return supportsFeature(stateObj, 4);
}
computeClassNames(stateObj) {

View File

@ -16,6 +16,7 @@ import computeStateDomain from "../../common/entity/compute_state_domain";
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";
const DOMAINS_NO_INFO = ["camera", "configurator", "history_graph"];
/*
@ -58,6 +59,11 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
:host([domain="camera"]) paper-dialog-scrollable {
margin: 0 -24px -21px;
}
:host([rtl]) app-toolbar {
direction: rtl;
text-align: right;
}
</style>
<app-toolbar>
@ -147,6 +153,11 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
hoursToShow: 24,
},
},
rtl: {
type: Boolean,
reflectToAttribute: true,
computed: "_computeRTL(hass)",
},
};
}
@ -190,5 +201,9 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
_gotoSettings() {
this.fire("more-info-page", { page: "settings" });
}
_computeRTL(hass) {
return computeRTL(hass);
}
}
customElements.define("more-info-controls", MoreInfoControls);

View File

@ -33,7 +33,7 @@
Home Assistant
</div>
<ha-onboarding>Initializing</ha-onboarding>
<ha-onboarding>Initializing</ha-onboarding>
</div>
<% if (!latestBuild) { %>
<script src="/static/custom-elements-es5-adapter.js"></script>

View File

@ -1,25 +0,0 @@
export default (superClass) =>
class extends superClass {
ready() {
super.ready();
this.addEventListener("register-dialog", (e) =>
this.registerDialog(e.detail)
);
}
registerDialog({ dialogShowEvent, dialogTag, dialogImport }) {
let loaded = null;
this.addEventListener(dialogShowEvent, (showEv) => {
if (!loaded) {
loaded = dialogImport().then(() => {
const dialogEl = document.createElement(dialogTag);
this.shadowRoot.appendChild(dialogEl);
this.provideHass(dialogEl);
return dialogEl;
});
}
loaded.then((dialogEl) => dialogEl.showDialog(showEv.detail));
});
}
};

View File

@ -0,0 +1,56 @@
import { PolymerElement } from "@polymer/polymer";
import { Constructor } from "@polymer/lit-element";
import { HASSDomEvent, ValidHassDomEvent } from "../../common/dom/fire_event";
interface RegisterDialogParams {
dialogShowEvent: keyof HASSDomEvents;
dialogTag: keyof HTMLElementTagNameMap;
dialogImport: () => Promise<unknown>;
}
interface HassDialog<T = HASSDomEvents[ValidHassDomEvent]> extends HTMLElement {
showDialog(params: T);
}
declare global {
// for fire event
interface HASSDomEvents {
"register-dialog": RegisterDialogParams;
}
// for add event listener
interface HTMLElementEventMap {
"register-dialog": HASSDomEvent<RegisterDialogParams>;
}
}
export const dialogManagerMixin = (superClass: Constructor<PolymerElement>) =>
class extends superClass {
public ready() {
super.ready();
this.addEventListener("register-dialog", (e) =>
this.registerDialog(e.detail)
);
}
private registerDialog({
dialogShowEvent,
dialogTag,
dialogImport,
}: RegisterDialogParams) {
let loaded: Promise<HassDialog<unknown>>;
this.addEventListener(dialogShowEvent, (showEv) => {
if (!loaded) {
loaded = dialogImport().then(() => {
const dialogEl = document.createElement(dialogTag) as HassDialog;
this.shadowRoot!.appendChild(dialogEl);
(this as any).provideHass(dialogEl);
return dialogEl;
});
}
loaded.then((dialogEl) =>
dialogEl.showDialog((showEv as HASSDomEvent<unknown>).detail)
);
});
}
};

View File

@ -4,6 +4,7 @@ import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { afterNextRender } from "@polymer/polymer/lib/utils/render-status";
import { html as litHtml, LitElement } from "@polymer/lit-element";
import "../home-assistant-main";
import "../ha-init-page";
@ -16,11 +17,13 @@ import TranslationsMixin from "./translations-mixin";
import ThemesMixin from "./themes-mixin";
import MoreInfoMixin from "./more-info-mixin";
import SidebarMixin from "./sidebar-mixin";
import DialogManagerMixin from "./dialog-manager-mixin";
import { dialogManagerMixin } from "./dialog-manager-mixin";
import ConnectionMixin from "./connection-mixin";
import NotificationMixin from "./notification-mixin";
import DisconnectToastMixin from "./disconnect-toast-mixin";
LitElement.prototype.html = litHtml;
const ext = (baseClass, mixins) =>
mixins.reduceRight((base, mixin) => mixin(base), baseClass);
@ -33,7 +36,7 @@ class HomeAssistant extends ext(PolymerElement, [
DisconnectToastMixin,
ConnectionMixin,
NotificationMixin,
DialogManagerMixin,
dialogManagerMixin,
HassBaseMixin,
]) {
static get template() {
@ -91,7 +94,7 @@ class HomeAssistant extends ext(PolymerElement, [
}
computePanelUrl(routeData) {
return (routeData && routeData.panel) || "states";
return (routeData && routeData.panel) || "lovelace";
}
panelUrlChanged(newPanelUrl) {

View File

@ -2,16 +2,15 @@ import "@polymer/app-layout/app-drawer-layout/app-drawer-layout";
import "@polymer/app-layout/app-drawer/app-drawer";
import "@polymer/app-route/app-route";
import "@polymer/iron-media-query/iron-media-query";
import "@polymer/iron-pages/iron-pages";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../util/ha-url-sync";
import "./partial-cards";
import "./partial-panel-resolver";
import EventsMixin from "../mixins/events-mixin";
import NavigateMixin from "../mixins/navigate-mixin";
import { computeRTL } from "../common/util/compute_rtl";
import(/* webpackChunkName: "ha-sidebar" */ "../components/ha-sidebar");
import(/* webpackChunkName: "voice-command-dialog" */ "../dialogs/ha-voice-command-dialog");
@ -30,21 +29,16 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
:host([rtl]) {
direction: rtl;
}
iron-pages,
partial-panel-resolver,
ha-sidebar {
/* allow a light tap highlight on the actual interface elements */
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
}
iron-pages {
partial-panel-resolver {
height: 100%;
}
</style>
<ha-url-sync hass="[[hass]]"></ha-url-sync>
<app-route
route="{{route}}"
pattern="/states"
tail="{{statesRouteTail}}"
></app-route>
<ha-voice-command-dialog
hass="[[hass]]"
id="voiceDialog"
@ -72,29 +66,12 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
></ha-sidebar>
</app-drawer>
<iron-pages
attr-for-selected="id"
fallback-selection="panel-resolver"
selected="[[hass.panelUrl]]"
selected-attribute="panel-visible"
>
<partial-cards
id="states"
narrow="[[narrow]]"
hass="[[hass]]"
show-menu="[[dockedSidebar]]"
route="[[statesRouteTail]]"
show-tabs=""
></partial-cards>
<partial-panel-resolver
id="panel-resolver"
narrow="[[narrow]]"
hass="[[hass]]"
route="[[route]]"
show-menu="[[dockedSidebar]]"
></partial-panel-resolver>
</iron-pages>
<partial-panel-resolver
narrow="[[narrow]]"
hass="[[hass]]"
route="[[route]]"
show-menu="[[dockedSidebar]]"
></partial-panel-resolver>
</app-drawer-layout>
`;
}
@ -107,7 +84,6 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
type: Object,
observer: "_routeChanged",
},
statesRouteTail: Object,
dockedSidebar: {
type: Boolean,
computed: "computeDockedSidebar(hass)",
@ -115,14 +91,14 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
rtl: {
type: Boolean,
reflectToAttribute: true,
computed: "computeRTL(hass)",
computed: "_computeRTL(hass)",
},
};
}
ready() {
super.ready();
this._defaultPage = localStorage.defaultPage || "states";
this._defaultPage = localStorage.defaultPage || "lovelace";
this.addEventListener("hass-open-menu", () => this.handleOpenMenu());
this.addEventListener("hass-close-menu", () => this.handleCloseMenu());
this.addEventListener("hass-start-voice", (ev) =>
@ -159,7 +135,7 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
connectedCallback() {
super.connectedCallback();
if (document.location.pathname === "/") {
this.navigate(`/${localStorage.defaultPage || "states"}`, true);
this.navigate(`/${localStorage.defaultPage || "lovelace"}`, true);
}
}
@ -175,12 +151,8 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
return NON_SWIPABLE_PANELS.indexOf(hass.panelUrl) !== -1;
}
computeRTL(hass) {
var lang = hass.selectedLanguage || hass.language;
if (hass.translationMetadata.translations[lang]) {
return hass.translationMetadata.translations[lang].isRTL || false;
}
return false;
_computeRTL(hass) {
return computeRTL(hass);
}
}

View File

@ -54,6 +54,10 @@ function ensureLoaded(panel) {
imported = import(/* webpackChunkName: "panel-lovelace" */ "../panels/lovelace/ha-panel-lovelace");
break;
case "states":
imported = import(/* webpackChunkName: "panel-states" */ "../panels/states/ha-panel-states");
break;
case "history":
imported = import(/* webpackChunkName: "panel-history" */ "../panels/history/ha-panel-history");
break;

View File

@ -24,6 +24,10 @@ export class CloudAlexaPref extends LitElement {
}
protected render(): TemplateResult {
if (!this.cloudStatus) {
return html``;
}
const enabled = this.cloudStatus!.prefs.alexa_enabled;
return html`

View File

@ -25,7 +25,11 @@ export class CloudGooglePref extends LitElement {
}
protected render(): TemplateResult {
const { google_enabled, google_allow_unlock } = this.cloudStatus!.prefs;
if (!this.cloudStatus) {
return html``;
}
const { google_enabled, google_allow_unlock } = this.cloudStatus.prefs;
return html`
${this.renderStyle()}

View File

@ -0,0 +1,142 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import "@polymer/paper-button/paper-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-dialog/paper-dialog";
// This is not a duplicate import, one is for types, one is for element.
// tslint:disable-next-line
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
// tslint:disable-next-line
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { buttonLink } from "../../../resources/ha-style";
import { HomeAssistant } from "../../../types";
import { WebhookDialogParams } from "./types";
const inputLabel = "Public URL Click to copy to clipboard";
export class CloudWebhookManageDialog extends LitElement {
protected hass?: HomeAssistant;
private _params?: WebhookDialogParams;
static get properties(): PropertyDeclarations {
return {
_params: {},
};
}
public async showDialog(params: WebhookDialogParams) {
this._params = params;
// Wait till dialog is rendered.
await this.updateComplete;
this._dialog.open();
}
protected render() {
if (!this._params) {
return html``;
}
const { webhook, cloudhook } = this._params;
const docsUrl =
webhook.domain === "automation"
? "https://www.home-assistant.io/docs/automation/trigger/#webhook-trigger"
: `https://www.home-assistant.io/components/${webhook.domain}/`;
return html`
${this._renderStyle()}
<paper-dialog with-backdrop>
<h2>Webhook for ${webhook.name}</h2>
<div>
<p>The webhook is available at the following url:</p>
<paper-input
label="${inputLabel}"
value="${cloudhook.cloudhook_url}"
@click="${this._copyClipboard}"
@blur="${this._restoreLabel}"
></paper-input>
<p>
If you no longer want to use this webhook, you can
<button class="link" @click="${this._disableWebhook}">
disable it</button
>.
</p>
</div>
<div class="paper-dialog-buttons">
<a href="${docsUrl}" target="_blank"
><paper-button>VIEW DOCUMENTATION</paper-button></a
>
<paper-button @click="${this._closeDialog}">CLOSE</paper-button>
</div>
</paper-dialog>
`;
}
private get _dialog(): PaperDialogElement {
return this.shadowRoot!.querySelector("paper-dialog")!;
}
private get _paperInput(): PaperInputElement {
return this.shadowRoot!.querySelector("paper-input")!;
}
private _closeDialog() {
this._dialog.close();
}
private async _disableWebhook() {
if (!confirm("Are you sure you want to disable this webhook?")) {
return;
}
this._params!.disableHook();
this._closeDialog();
}
private _copyClipboard(ev: FocusEvent) {
// paper-input -> iron-input -> input
const paperInput = ev.currentTarget as PaperInputElement;
const input = (paperInput.inputElement as any)
.inputElement as HTMLInputElement;
input.setSelectionRange(0, input.value.length);
try {
document.execCommand("copy");
paperInput.label = "COPIED TO CLIPBOARD";
} catch (err) {
// Copying failed. Oh no
}
}
private _restoreLabel() {
this._paperInput.label = inputLabel;
}
private _renderStyle() {
return html`
<style>
paper-dialog {
width: 650px;
}
paper-input {
margin-top: -8px;
}
${buttonLink} button.link {
color: var(--primary-color);
}
paper-button {
color: var(--primary-color);
font-weight: 500;
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"cloud-webhook-manage-dialog": CloudWebhookManageDialog;
}
}
customElements.define("cloud-webhook-manage-dialog", CloudWebhookManageDialog);

View File

@ -0,0 +1,234 @@
import {
html,
LitElement,
PropertyDeclarations,
PropertyValues,
} from "@polymer/lit-element";
import "@polymer/paper-toggle-button/paper-toggle-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-spinner/paper-spinner";
import "../../../components/ha-card";
import { fireEvent } from "../../../common/dom/fire_event";
import { HomeAssistant, WebhookError } from "../../../types";
import { WebhookDialogParams, CloudStatusLoggedIn } from "./types";
import { Webhook, fetchWebhooks } from "../../../data/webhook";
import {
createCloudhook,
deleteCloudhook,
CloudWebhook,
} from "../../../data/cloud";
declare global {
// for fire event
interface HASSDomEvents {
"manage-cloud-webhook": WebhookDialogParams;
}
}
export class CloudWebhooks extends LitElement {
public hass?: HomeAssistant;
public cloudStatus?: CloudStatusLoggedIn;
private _cloudHooks?: { [webhookId: string]: CloudWebhook };
private _localHooks?: Webhook[];
private _progress: string[];
static get properties(): PropertyDeclarations {
return {
hass: {},
cloudStatus: {},
_cloudHooks: {},
_localHooks: {},
_progress: {},
};
}
constructor() {
super();
this._progress = [];
}
public connectedCallback() {
super.connectedCallback();
this._fetchData();
}
protected render() {
return html`
${this.renderStyle()}
<ha-card header="Webhooks">
<div class="body">
Anything that is configured to be triggered by a webhook can be given
a publicly accessible URL to allow you to send data back to Home
Assistant from anywhere, without exposing your instance to the
internet.
</div>
${this._renderBody()}
<div class="footer">
<a href="https://www.nabucasa.com/config/webhooks" target="_blank">
Learn more about creating webhook-powered automations.
</a>
</div>
</ha-card>
`;
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("cloudStatus") && this.cloudStatus) {
this._cloudHooks = this.cloudStatus.prefs.cloudhooks || {};
}
}
private _renderBody() {
if (!this.cloudStatus || !this._localHooks || !this._cloudHooks) {
return html`
<div class="loading">Loading</div>
`;
}
return this._localHooks.map(
(entry) => html`
<div class="webhook" .entry="${entry}">
<paper-item-body two-line>
<div>
${entry.name}
${
entry.domain === entry.name.toLowerCase()
? ""
: ` (${entry.domain})`
}
</div>
<div secondary>${entry.webhook_id}</div>
</paper-item-body>
${
this._progress.includes(entry.webhook_id)
? html`
<div class="progress">
<paper-spinner active></paper-spinner>
</div>
`
: this._cloudHooks![entry.webhook_id]
? html`
<paper-button @click="${this._handleManageButton}"
>Manage</paper-button
>
`
: html`
<paper-toggle-button
@click="${this._enableWebhook}"
></paper-toggle-button>
`
}
</div>
`
);
}
private _showDialog(webhookId: string) {
const webhook = this._localHooks!.find(
(ent) => ent.webhook_id === webhookId
);
const cloudhook = this._cloudHooks![webhookId];
const params: WebhookDialogParams = {
webhook: webhook!,
cloudhook,
disableHook: () => this._disableWebhook(webhookId),
};
fireEvent(this, "manage-cloud-webhook", params);
}
private _handleManageButton(ev: MouseEvent) {
const entry = (ev.currentTarget as any).parentElement.entry as Webhook;
this._showDialog(entry.webhook_id);
}
private async _enableWebhook(ev: MouseEvent) {
const entry = (ev.currentTarget as any).parentElement.entry;
this._progress = [...this._progress, entry.webhook_id];
let updatedWebhook;
try {
updatedWebhook = await createCloudhook(this.hass!, entry.webhook_id);
} catch (err) {
alert((err as WebhookError).message);
return;
} finally {
this._progress = this._progress.filter((wid) => wid !== entry.webhook_id);
}
this._cloudHooks = {
...this._cloudHooks,
[entry.webhook_id]: updatedWebhook,
};
// Only open dialog if we're not also enabling others, otherwise it's confusing
if (this._progress.length === 0) {
this._showDialog(entry.webhook_id);
}
}
private async _disableWebhook(webhookId: string) {
this._progress = [...this._progress, webhookId];
try {
await deleteCloudhook(this.hass!, webhookId!);
} catch (err) {
alert(`Failed to disable webhook: ${(err as WebhookError).message}`);
return;
} finally {
this._progress = this._progress.filter((wid) => wid !== webhookId);
}
// Remove cloud related parts from entry.
const { [webhookId]: disabledHook, ...newHooks } = this._cloudHooks!;
this._cloudHooks = newHooks;
}
private async _fetchData() {
this._localHooks = await fetchWebhooks(this.hass!);
}
private renderStyle() {
return html`
<style>
.body {
padding: 0 16px 8px;
}
.loading {
padding: 0 16px;
}
.webhook {
display: flex;
padding: 4px 16px;
}
.progress {
margin-right: 16px;
display: flex;
flex-direction: column;
justify-content: center;
}
paper-button {
font-weight: 500;
color: var(--primary-color);
}
.footer {
padding: 16px;
}
.footer a {
color: var(--primary-color);
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"cloud-webhooks": CloudWebhooks;
}
}
customElements.define("cloud-webhooks", CloudWebhooks);

View File

@ -10,15 +10,19 @@ import "../../../layouts/hass-subpage";
import "../../../resources/ha-style";
import "../ha-config-section";
import "./cloud-webhooks";
import formatDateTime from "../../../common/datetime/format_date_time";
import EventsMixin from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
import { fireEvent } from "../../../common/dom/fire_event";
import { fetchSubscriptionInfo } from "./data";
import "./cloud-alexa-pref";
import "./cloud-google-pref";
let registeredWebhookDialog = false;
/*
* @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
@ -129,6 +133,11 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
hass="[[hass]]"
cloud-status="[[cloudStatus]]"
></cloud-google-pref>
<cloud-webhooks
hass="[[hass]]"
cloud-status="[[cloudStatus]]"
></cloud-webhooks>
</ha-config-section>
</div>
</hass-subpage>
@ -152,9 +161,26 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
this._fetchSubscriptionInfo();
}
connectedCallback() {
super.connectedCallback();
if (!registeredWebhookDialog) {
registeredWebhookDialog = true;
fireEvent(this, "register-dialog", {
dialogShowEvent: "manage-cloud-webhook",
dialogTag: "cloud-webhook-manage-dialog",
dialogImport: () => import("./cloud-webhook-manage-dialog"),
});
}
}
async _fetchSubscriptionInfo() {
this._subscription = await fetchSubscriptionInfo(this.hass);
if (this._subscription.provider && this.cloudStatus.cloud !== "connected") {
if (
this._subscription.provider &&
this.cloudStatus &&
this.cloudStatus.cloud !== "connected"
) {
this.fire("ha-refresh-cloud-status");
}
}

View File

@ -1,3 +1,6 @@
import { CloudWebhook } from "../../../data/cloud";
import { Webhook } from "../../../data/webhook";
export interface EntityFilter {
include_domains: string[];
include_entities: string[];
@ -19,6 +22,7 @@ export type CloudStatusLoggedIn = CloudStatusBase & {
google_enabled: boolean;
alexa_enabled: boolean;
google_allow_unlock: boolean;
cloudhooks: { [webhookId: string]: CloudWebhook };
};
};
@ -27,3 +31,9 @@ export type CloudStatus = CloudStatusBase | CloudStatusLoggedIn;
export interface SubscriptionInfo {
human_description: string;
}
export interface WebhookDialogParams {
webhook: Webhook;
cloudhook: CloudWebhook;
disableHook: () => void;
}

View File

@ -24,7 +24,18 @@ export default class NumericStateTrigger extends Component {
/* eslint-disable camelcase */
render({ trigger, hass, localize }) {
const { value_template, entity_id, below, above } = trigger;
let trgFor = trigger.for;
if (trgFor && (trgFor.hours || trgFor.minutes || trgFor.seconds)) {
// If the trigger was defined using the yaml dict syntax, convert it to
// the equivalent string format
let { hours = 0, minutes = 0, seconds = 0 } = trgFor;
hours = hours.toString();
minutes = minutes.toString().padStart(2, "0");
seconds = seconds.toString().padStart(2, "0");
trgFor = `${hours}:${minutes}:${seconds}`;
}
return (
<div>
<ha-entity-picker
@ -57,6 +68,14 @@ export default class NumericStateTrigger extends Component {
value={value_template}
onvalue-changed={this.onChange}
/>
<paper-input
label={localize(
"ui.panel.config.automation.editor.triggers.type.state.for"
)}
name="for"
value={trgFor}
onvalue-changed={this.onChange}
/>
</div>
);
}

View File

@ -164,7 +164,7 @@ class HaPanelDevInfo extends EventsMixin(LocalizeMixin(PolymerElement)) {
</template>
</p>
<p>
<a href='/lovelace'>Try out the new Lovelace UI (experimental)</a>
<a href='/states'>Go back to the old states page</a>
<div id="love" style="cursor:pointer;" on-click="_toggleDefaultPage">[[_defaultPageText()]]</div
</p>
</div>
@ -364,15 +364,15 @@ class HaPanelDevInfo extends EventsMixin(LocalizeMixin(PolymerElement)) {
_defaultPageText() {
return `>> ${
localStorage.defaultPage === "lovelace" ? "Remove" : "Set"
} lovelace as default page on this device <<`;
localStorage.defaultPage === "states" ? "Remove" : "Set"
} the old states as default page on this device <<`;
}
_toggleDefaultPage() {
if (localStorage.defaultPage === "lovelace") {
if (localStorage.defaultPage === "states") {
delete localStorage.defaultPage;
} else {
localStorage.defaultPage = "lovelace";
localStorage.defaultPage = "states";
}
this.$.love.innerText = this._defaultPageText();
}

View File

@ -1,18 +1,18 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../layouts/partial-cards";
import "../states/ha-panel-states";
class HaPanelKiosk extends PolymerElement {
static get template() {
return html`
<partial-cards
<ha-panel-states
id="kiosk-states"
hass="[[hass]]"
show-menu
route="[[route]]"
panel-visible
></partial-cards>
></ha-panel-states>
`;
}

View File

@ -209,8 +209,8 @@ class HuiAlarmPanelCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
_computeHeader(localize, stateObj) {
if (!stateObj) return "";
return this._config.title
? this._config.title
return this._config.name
? this._config.name
: this._label(localize, stateObj.state);
}

View File

@ -1,7 +1,8 @@
import createCardElement from "../common/create-card-element";
import { computeCardSize } from "../common/compute-card-size";
import { HomeAssistant } from "../../../types";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
interface Condition {
entity: string;
@ -9,8 +10,8 @@ interface Condition {
state_not?: string;
}
interface Config extends LovelaceConfig {
card: LovelaceConfig;
interface Config extends LovelaceCardConfig {
card: LovelaceCardConfig;
conditions: Condition[];
}

View File

@ -14,8 +14,9 @@ import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../types";
import { EntityConfig, EntityRow } from "../entity-rows/types";
import { LovelaceCard, LovelaceConfig, LovelaceCardEditor } from "../types";
import processConfigEntities from "../common/process-config-entities";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { processConfigEntities } from "../common/process-config-entities";
import createRowElement from "../common/create-row-element";
import computeDomain from "../../../common/entity/compute_domain";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
@ -29,7 +30,7 @@ export interface ConfigEntity extends EntityConfig {
url?: string;
}
export interface Config extends LovelaceConfig {
export interface Config extends LovelaceCardConfig {
show_header_toggle?: boolean;
title?: string;
entities: ConfigEntity[];
@ -42,6 +43,11 @@ class HuiEntitiesCard extends hassLocalizeLitMixin(LitElement)
await import("../editor/config-elements/hui-entities-card-editor");
return document.createElement("hui-entities-card-editor");
}
public static getStubConfig(): object {
return { entities: [] };
}
protected _hass?: HomeAssistant;
protected _config?: Config;
protected _configEntities?: ConfigEntity[];

View File

@ -10,7 +10,6 @@ import { styleMap } from "lit-html/directives/styleMap";
import "../../../components/ha-card";
import toggleEntity from "../common/entity/toggle-entity";
import isValidEntityId from "../../../common/entity/valid_entity_id";
import stateIcon from "../../../common/entity/state_icon";
import computeStateDomain from "../../../common/entity/compute_state_domain";
@ -18,19 +17,18 @@ import computeStateName from "../../../common/entity/compute_state_name";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import { HomeAssistant, LightEntity } from "../../../types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
import { longPress } from "../common/directives/long-press-directive";
import { fireEvent } from "../../../common/dom/fire_event";
import { handleClick } from "../common/handle-click";
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
entity: string;
name?: string;
icon?: string;
theme?: string;
tap_action?: "toggle" | "call-service" | "more-info";
hold_action?: "toggle" | "call-service" | "more-info";
service?: string;
service_data?: object;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
}
class HuiEntityButtonCard extends hassLocalizeLitMixin(LitElement)
@ -81,8 +79,8 @@ class HuiEntityButtonCard extends hassLocalizeLitMixin(LitElement)
return html`
${this.renderStyle()}
<ha-card
@ha-click="${() => this.handleClick(false)}"
@ha-hold="${() => this.handleClick(true)}"
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
>
${
@ -186,34 +184,12 @@ class HuiEntityButtonCard extends hassLocalizeLitMixin(LitElement)
return `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
}
private handleClick(hold: boolean): void {
const config = this._config;
if (!config) {
return;
}
const stateObj = this.hass!.states[config.entity];
if (!stateObj) {
return;
}
const entityId = stateObj.entity_id;
const action = hold ? config.hold_action : config.tap_action || "more-info";
switch (action) {
case "toggle":
toggleEntity(this.hass, entityId);
break;
case "call-service":
if (!config.service) {
return;
}
const [domain, service] = config.service.split(".", 2);
const serviceData = { entity_id: entityId, ...config.service_data };
this.hass!.callService(domain, service, serviceData);
break;
case "more-info":
fireEvent(this, "hass-more-info", { entityId });
break;
default:
}
private _handleTap() {
handleClick(this, this.hass!, this._config!, false);
}
private _handleHold() {
handleClick(this, this.hass!, this._config!, true);
}
}

View File

@ -1,7 +1,7 @@
import { PolymerElement } from "@polymer/polymer/polymer-element";
import createCardElement from "../common/create-card-element";
import processConfigEntities from "../common/process-config-entities";
import { processConfigEntities } from "../common/process-config-entities";
function getEntities(hass, filterState, entities) {
return entities.filter((entityConf) => {

View File

@ -1,11 +1,12 @@
import { html, LitElement } from "@polymer/lit-element";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { TemplateResult } from "lit-html";
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
error: string;
origConfig: LovelaceConfig;
origConfig: LovelaceCardConfig;
}
class HuiErrorCard extends LitElement implements LovelaceCard {
@ -50,7 +51,7 @@ class HuiErrorCard extends LitElement implements LovelaceCard {
`;
}
private _toStr(config: LovelaceConfig): string {
private _toStr(config: LovelaceCardConfig): string {
return JSON.stringify(config, null, 2);
}
}

View File

@ -6,20 +6,22 @@ import {
} from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import isValidEntityId from "../../../common/entity/valid_entity_id";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import computeStateName from "../../../common/entity/compute_state_name";
import "../../../components/ha-card";
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
entity: string;
title?: string;
unit_of_measurement?: string;
name?: string;
unit?: string;
min?: number;
max?: number;
severity?: object;
@ -87,12 +89,14 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
<div id="percent">
${stateObj.state}
${
this._config.unit_of_measurement ||
this._config.unit ||
stateObj.attributes.unit_of_measurement ||
""
}
</div>
<div id="title">${this._config.title}</div>
<div id="name">
${this._config.name || computeStateName(stateObj)}
</div>
</div>
</div>
`
@ -210,7 +214,7 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
.gauge-data #percent {
font-size: calc(var(--base-unit) * 0.55);
}
.gauge-data #title {
.gauge-data #name {
padding-top: calc(var(--base-unit) * 0.15);
font-size: calc(var(--base-unit) * 0.3);
}

View File

@ -7,31 +7,29 @@ import {
import { TemplateResult } from "lit-html";
import { classMap } from "lit-html/directives/classMap";
import { fireEvent } from "../../../common/dom/fire_event";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../types";
import { LovelaceCard, LovelaceConfig, LovelaceCardEditor } from "../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
import { longPress } from "../common/directives/long-press-directive";
import { EntityConfig } from "../entity-rows/types";
import { processConfigEntities } from "../common/process-config-entities";
import computeStateDisplay from "../../../common/entity/compute_state_display";
import computeStateName from "../../../common/entity/compute_state_name";
import processConfigEntities from "../common/process-config-entities";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import toggleEntity from "../common/entity/toggle-entity";
import "../../../components/entity/state-badge";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import { handleClick } from "../common/handle-click";
export interface ConfigEntity extends EntityConfig {
tap_action?: "toggle" | "call-service" | "more-info";
hold_action?: "toggle" | "call-service" | "more-info";
service?: string;
service_data?: object;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
}
export interface Config extends LovelaceConfig {
export interface Config extends LovelaceCardConfig {
show_name?: boolean;
show_state?: boolean;
title?: string;
@ -46,6 +44,9 @@ export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
await import("../editor/config-elements/hui-glance-card-editor");
return document.createElement("hui-glance-card-editor");
}
public static getStubConfig(): object {
return { entities: [] };
}
public hass?: HomeAssistant;
private _config?: Config;
@ -67,13 +68,16 @@ export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
public setConfig(config: Config): void {
this._config = { theme: "default", ...config };
const entities = processConfigEntities(config.entities);
const entities = processConfigEntities<ConfigEntity>(config.entities);
for (const entity of entities) {
if (
(entity.tap_action === "call-service" ||
entity.hold_action === "call-service") &&
!entity.service
(entity.tap_action &&
entity.tap_action.action === "call-service" &&
!entity.tap_action.service) ||
(entity.hold_action &&
entity.hold_action.action === "call-service" &&
!entity.hold_action.service)
) {
throw new Error(
'Missing required property "service" when tap_action or hold_action is call-service'
@ -199,8 +203,8 @@ export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
<div
class="entity"
.entityConf="${entityConf}"
@ha-click="${(ev) => this.handleClick(ev, false)}"
@ha-hold="${(ev) => this.handleClick(ev, true)}"
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
>
${
@ -239,24 +243,14 @@ export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
`;
}
private handleClick(ev: MouseEvent, hold: boolean): void {
private _handleTap(ev: MouseEvent) {
const config = (ev.currentTarget as any).entityConf as ConfigEntity;
const entityId = config.entity;
const action = hold ? config.hold_action : config.tap_action || "more-info";
switch (action) {
case "toggle":
toggleEntity(this.hass, entityId);
break;
case "call-service":
const [domain, service] = config.service!.split(".", 2);
const serviceData = { entity_id: entityId, ...config.service_data };
this.hass!.callService(domain, service, serviceData);
break;
case "more-info":
fireEvent(this, "hass-more-info", { entityId });
break;
default:
}
handleClick(this, this.hass!, config, false);
}
private _handleHold(ev: MouseEvent) {
const config = (ev.currentTarget as any).entityConf as ConfigEntity;
handleClick(this, this.hass!, config, true);
}
}

View File

@ -5,7 +5,7 @@ import "../../../components/ha-card";
import "../../../components/state-history-charts";
import "../../../data/ha-state-history-data";
import processConfigEntities from "../common/process-config-entities";
import { processConfigEntities } from "../common/process-config-entities";
class HuiHistoryGraphCard extends PolymerElement {
static get template() {

View File

@ -2,11 +2,12 @@ import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import "../../../components/ha-card";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { TemplateResult } from "lit-html";
import { styleMap } from "lit-html/directives/styleMap";
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
aspect_ratio?: string;
title?: string;
url: string;

View File

@ -34,6 +34,7 @@ export default class LegacyWrapperCard extends HTMLElement {
this._ensureElement(this._tag);
this.lastChild.hass = hass;
this.lastChild.stateObj = hass.states[entityId];
this.lastChild.config = this._config;
} else {
this._ensureElement("HUI-ERROR-CARD");
this.lastChild.setConfig(

View File

@ -12,7 +12,8 @@ import { jQuery } from "../../../resources/jquery";
import { roundSliderStyle } from "../../../resources/jquery.roundslider";
import { HomeAssistant, LightEntity } from "../../../types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { longPress } from "../common/directives/long-press-directive";
import stateIcon from "../../../common/entity/state_icon";
@ -37,7 +38,7 @@ const lightConfig = {
showTooltip: false,
};
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
entity: string;
name?: string;
theme?: string;

View File

@ -6,10 +6,11 @@ import Leaflet from "leaflet";
import "../../map/ha-entity-marker";
import setupLeafletMap from "../../../common/dom/setup-leaflet-map";
import processConfigEntities from "../common/process-config-entities";
import { processConfigEntities } from "../common/process-config-entities";
import computeStateDomain from "../../../common/entity/compute_state_domain";
import computeStateName from "../../../common/entity/compute_state_name";
import debounce from "../../../common/util/debounce";
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
Leaflet.Icon.Default.imagePath = "/static/images/leaflet";
@ -97,7 +98,15 @@ class HuiMapCard extends PolymerElement {
return;
}
this.$.root.style.paddingTop = this._config.aspect_ratio || "100%";
const ratio = parseAspectRatio(this._config.aspect_ratio);
if (ratio && ratio.w > 0 && ratio.h > 0) {
this.$.root.style.paddingBottom = `${((100 * ratio.h) / ratio.w).toFixed(
2
)}%`;
} else {
this.$.root.style.paddingBottom = "100%";
}
}
setConfig(config) {
@ -110,8 +119,13 @@ class HuiMapCard extends PolymerElement {
}
getCardSize() {
let ar = this._config.aspect_ratio || "100%";
ar = ar.substr(0, ar.length - 1);
const ratio = parseAspectRatio(this._config.aspect_ratio);
let ar;
if (ratio && ratio.w > 0 && ratio.h > 0) {
ar = `${((100 * ratio.h) / ratio.w).toFixed(2)}`;
} else {
ar = "100";
}
return 1 + Math.floor(ar / 25) || 3;
}

View File

@ -4,10 +4,11 @@ import { classMap } from "lit-html/directives/classMap";
import "../../../components/ha-card";
import "../../../components/ha-markdown";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { TemplateResult } from "lit-html";
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
content: string;
title?: string;
}

View File

@ -2,17 +2,18 @@ import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import "../../../components/ha-card";
import { LovelaceCard, LovelaceConfig } from "../types";
import { navigate } from "../../../common/navigate";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { TemplateResult } from "lit-html";
import { classMap } from "lit-html/directives/classMap";
import { handleClick } from "../common/handle-click";
import { longPress } from "../common/directives/long-press-directive";
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
image?: string;
navigation_path?: string;
service?: string;
service_data?: object;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
}
export class HuiPictureCard extends LitElement implements LovelaceCard {
@ -45,11 +46,13 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
return html`
${this.renderStyle()}
<ha-card
@click="${this.handleClick}"
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
class="${
classMap({
clickable: Boolean(
this._config.navigation_path || this._config.service
this._config.tap_action || this._config.hold_action
),
})
}"
@ -76,14 +79,12 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
`;
}
private handleClick(): void {
if (this._config!.navigation_path) {
navigate(this, this._config!.navigation_path!);
}
if (this._config!.service) {
const [domain, service] = this._config!.service!.split(".", 2);
this.hass!.callService(domain, service, this._config!.service_data);
}
private _handleTap() {
handleClick(this, this.hass!, this._config!, false);
}
private _handleHold() {
handleClick(this, this.hass!, this._config!, true);
}
}

View File

@ -3,13 +3,18 @@ import { TemplateResult } from "lit-html";
import createHuiElement from "../common/create-hui-element";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { LovelaceElementConfig, LovelaceElement } from "../elements/types";
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
title?: string;
image: string;
image?: string;
camera_image?: string;
state_image?: {};
aspect_ratio?: string;
entity?: string;
elements: LovelaceElementConfig[];
}
@ -38,7 +43,10 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
public setConfig(config: Config): void {
if (!config) {
throw new Error("Invalid Configuration");
} else if (!config.image) {
} else if (
!(config.image || config.camera_image || config.state_image) ||
(config.state_image && !config.entity)
) {
throw new Error("Invalid Configuration: image required");
} else if (!Array.isArray(config.elements)) {
throw new Error("Invalid Configuration: elements required");
@ -55,13 +63,19 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
return html`
${this.renderStyle()}
<ha-card .header="${this._config.title}">
<div id="root">
<img src="${this._config.image}" /> ${
this._config.elements.map((elementConfig: LovelaceElementConfig) =>
this._createHuiElement(elementConfig)
)
}
</div>
<hui-image
.hass="${this._hass}"
.image="${this._config.image}"
.stateImage="${this._config.state_image}"
.cameraImage="${this._config.camera_image}"
.entity="${this._config.entity}"
.aspectRatio="${this._config.aspect_ratio}"
></hui-image>
${
this._config.elements.map((elementConfig: LovelaceElementConfig) =>
this._createHuiElement(elementConfig)
)
}
</ha-card>
`;
}
@ -71,14 +85,7 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
<style>
ha-card {
overflow: hidden;
}
#root {
position: relative;
overflow: hidden;
}
#root img {
display: block;
width: 100%;
}
.element {
position: absolute;

View File

@ -1,197 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-card";
import "../components/hui-image";
import computeDomain from "../../../common/entity/compute_domain";
import computeStateDisplay from "../../../common/entity/compute_state_display";
import computeStateName from "../../../common/entity/compute_state_name";
import toggleEntity from "../common/entity/toggle-entity";
import EventsMixin from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
import { longPressBind } from "../common/directives/long-press-directive";
const UNAVAILABLE = "Unavailable";
/*
* @appliesMixin LocalizeMixin
* @appliesMixin EventsMixin
*/
class HuiPictureEntityCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
static get template() {
return html`
<style>
ha-card {
min-height: 75px;
overflow: hidden;
position: relative;
}
ha-card.canInteract {
cursor: pointer;
}
.footer {
@apply --paper-font-common-nowrap;
position: absolute;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
padding: 16px;
font-size: 16px;
line-height: 16px;
color: white;
}
.both {
display: flex;
justify-content: space-between;
}
.state {
text-align: right;
}
</style>
<ha-card id="card">
<hui-image
hass="[[hass]]"
image="[[_config.image]]"
state-image="[[_config.state_image]]"
camera-image="[[_getCameraImage(_config)]]"
entity="[[_config.entity]]"
aspect-ratio="[[_config.aspect_ratio]]"
></hui-image>
<template is="dom-if" if="[[_showNameAndState(_config)]]">
<div class="footer both">
<div>[[_name]]</div>
<div>[[_state]]</div>
</div>
</template>
<template is="dom-if" if="[[_showName(_config)]]">
<div class="footer">[[_name]]</div>
</template>
<template is="dom-if" if="[[_showState(_config)]]">
<div class="footer state">[[_state]]</div>
</template>
</ha-card>
`;
}
static get properties() {
return {
hass: {
type: Object,
observer: "_hassChanged",
},
_config: Object,
_name: String,
_state: String,
};
}
getCardSize() {
return 3;
}
setConfig(config) {
if (!config || !config.entity) {
throw new Error("Error in card configuration.");
}
this._entityDomain = computeDomain(config.entity);
if (
this._entityDomain !== "camera" &&
(!config.image && !config.state_image && !config.camera_image)
) {
throw new Error("No image source configured.");
}
this._config = config;
}
ready() {
super.ready();
const card = this.shadowRoot.querySelector("#card");
longPressBind(card);
card.addEventListener("ha-click", () => this._cardClicked(false));
card.addEventListener("ha-hold", () => this._cardClicked(true));
}
_hassChanged(hass) {
const config = this._config;
const entityId = config.entity;
const stateObj = hass.states[entityId];
// Nothing changed
if (
(!stateObj && this._oldState === UNAVAILABLE) ||
(stateObj && stateObj.state === this._oldState)
) {
return;
}
let name;
let state;
let stateLabel;
let available;
if (stateObj) {
name = config.name || computeStateName(stateObj);
state = stateObj.state;
stateLabel = computeStateDisplay(this.localize, stateObj);
available = true;
} else {
name = config.name || entityId;
state = UNAVAILABLE;
stateLabel = this.localize("state.default.unavailable");
available = false;
}
this.setProperties({
_name: name,
_state: stateLabel,
_oldState: state,
});
this.$.card.classList.toggle("canInteract", available);
}
_showNameAndState(config) {
return config.show_name !== false && config.show_state !== false;
}
_showName(config) {
return config.show_name !== false && config.show_state === false;
}
_showState(config) {
return config.show_name === false && config.show_state !== false;
}
_cardClicked(hold) {
const config = this._config;
const entityId = config.entity;
if (!(entityId in this.hass.states)) return;
const action = hold ? config.hold_action : config.tap_action || "more-info";
switch (action) {
case "toggle":
toggleEntity(this.hass, entityId);
break;
case "more-info":
this.fire("hass-more-info", { entityId });
break;
default:
}
}
_getCameraImage(config) {
return this._entityDomain === "camera"
? config.entity
: config.camera_image;
}
}
customElements.define("hui-picture-entity-card", HuiPictureEntityCard);

View File

@ -0,0 +1,172 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html/lib/shady-render";
import { classMap } from "lit-html/directives/classMap";
import "../../../components/ha-card";
import "../components/hui-image";
import computeDomain from "../../../common/entity/compute_domain";
import computeStateDisplay from "../../../common/entity/compute_state_display";
import computeStateName from "../../../common/entity/compute_state_name";
import { longPress } from "../common/directives/long-press-directive";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../types";
import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
import { LovelaceCard } from "../types";
import { handleClick } from "../common/handle-click";
import { UNAVAILABLE } from "../../../data/entity";
interface Config extends LovelaceCardConfig {
entity: string;
name?: string;
image?: string;
camera_image?: string;
state_image?: {};
aspect_ratio?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
show_name?: boolean;
show_state?: boolean;
}
class HuiPictureEntityCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard {
public hass?: HomeAssistant;
private _config?: Config;
static get properties(): PropertyDeclarations {
return {
hass: {},
_config: {},
};
}
public getCardSize(): number {
return 3;
}
public setConfig(config: Config): void {
if (!config || !config.entity) {
throw new Error("Invalid Configuration: 'entity' required");
}
if (
computeDomain(config.entity) !== "camera" &&
(!config.image && !config.state_image && !config.camera_image)
) {
throw new Error("No image source configured.");
}
this._config = { show_name: true, show_state: true, ...config };
}
protected render(): TemplateResult {
if (!this._config || !this.hass || !this.hass.states[this._config.entity]) {
return html``;
}
const stateObj = this.hass.states[this._config.entity];
const name = this._config.name || computeStateName(stateObj);
const state = computeStateDisplay(
this.localize,
stateObj,
this.hass.language
);
let footer: TemplateResult | string = "";
if (this._config.show_name && this._config.show_state) {
footer = html`
<div class="footer both">
<div>${name}</div>
<div>${state}</div>
</div>
`;
} else if (this._config.show_name) {
footer = html`
<div class="footer">${name}</div>
`;
} else if (this._config.show_state) {
footer = html`
<div class="footer state">${state}</div>
`;
}
return html`
${this.renderStyle()}
<ha-card>
<hui-image
.hass="${this.hass}"
.image="${this._config.image}"
.stateImage="${this._config.state_image}"
.cameraImage="${
computeDomain(this._config.entity) === "camera"
? this._config.entity
: this._config.camera_image
}"
.entity="${this._config.entity}"
.aspectRatio="${this._config.aspect_ratio}"
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
class="${
classMap({
clickable: stateObj.state !== UNAVAILABLE,
})
}"
></hui-image>
${footer}
</ha-card>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
ha-card {
min-height: 75px;
overflow: hidden;
position: relative;
}
hui-image.clickable {
cursor: pointer;
}
.footer {
@apply --paper-font-common-nowrap;
position: absolute;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
padding: 16px;
font-size: 16px;
line-height: 16px;
color: white;
}
.both {
display: flex;
justify-content: space-between;
}
.state {
text-align: right;
}
</style>
`;
}
private _handleTap() {
handleClick(this, this.hass!, this._config!, false);
}
private _handleHold() {
handleClick(this, this.hass!, this._config!, true);
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-picture-entity-card": HuiPictureEntityCard;
}
}
customElements.define("hui-picture-entity-card", HuiPictureEntityCard);

View File

@ -3,36 +3,37 @@ import { classMap } from "lit-html/directives/classMap";
import { TemplateResult } from "lit-html";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { fireEvent } from "../../../common/dom/fire_event";
import { DOMAINS_TOGGLE } from "../../../common/const";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
import { EntityConfig } from "../entity-rows/types";
import { navigate } from "../../../common/navigate";
import { HomeAssistant } from "../../../types";
import { longPress } from "../common/directives/long-press-directive";
import { processConfigEntities } from "../common/process-config-entities";
import computeStateDisplay from "../../../common/entity/compute_state_display";
import computeStateName from "../../../common/entity/compute_state_name";
import processConfigEntities from "../common/process-config-entities";
import computeDomain from "../../../common/entity/compute_domain";
import stateIcon from "../../../common/entity/state_icon";
import toggleEntity from "../common/entity/toggle-entity";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import "../components/hui-image";
import { handleClick } from "../common/handle-click";
import { fireEvent } from "../../../common/dom/fire_event";
import { toggleEntity } from "../common/entity/toggle-entity";
const STATES_OFF = new Set(["closed", "locked", "not_home", "off"]);
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
entities: EntityConfig[];
title?: string;
navigation_path?: string;
image?: string;
camera_image?: string;
state_image?: {};
aspect_ratio?: string;
entity?: string;
force_dialog?: boolean;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
}
class HuiPictureGlanceCard extends hassLocalizeLitMixin(LitElement)
@ -87,19 +88,22 @@ class HuiPictureGlanceCard extends hassLocalizeLitMixin(LitElement)
return html``;
}
const isClickable =
this._config.navigation_path || this._config.camera_image;
return html`
${this.renderStyle()}
<ha-card>
<hui-image
class="${
classMap({
clickable: Boolean(isClickable),
clickable: Boolean(
this._config.tap_action ||
this._config.hold_action ||
this._config.camera_image
),
})
}"
@click="${this._handleImageClick}"
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
.hass="${this.hass}"
.image="${this._config.image}"
.stateImage="${this._config.state_image}"
@ -167,22 +171,20 @@ class HuiPictureGlanceCard extends hassLocalizeLitMixin(LitElement)
`;
}
private _handleTap() {
handleClick(this, this.hass!, this._config!, false);
}
private _handleHold() {
handleClick(this, this.hass!, this._config!, true);
}
private _openDialog(ev: MouseEvent): void {
fireEvent(this, "hass-more-info", { entityId: (ev.target as any).entity });
}
private _callService(ev: MouseEvent): void {
toggleEntity(this.hass, (ev.target as any).entity);
}
private _handleImageClick(): void {
if (this._config!.navigation_path) {
navigate(this, this._config!.navigation_path!);
} else if (this._config!.camera_image) {
fireEvent(this, "hass-more-info", {
entityId: this._config!.camera_image,
});
}
toggleEntity(this.hass!, (ev.target as any).entity);
}
private renderStyle(): TemplateResult {

View File

@ -1,320 +0,0 @@
import { LitElement, html, svg } from "@polymer/lit-element";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import computeStateName from "../../../common/entity/compute_state_name";
import stateIcon from "../../../common/entity/state_icon";
import EventsMixin from "../../../mixins/events-mixin";
class HuiSensorCard extends EventsMixin(LitElement) {
set hass(hass) {
this._hass = hass;
const entity = hass.states[this._config.entity];
if (entity && this._entity !== entity) {
this._entity = entity;
if (
this._config.graph !== "none" &&
entity.attributes.unit_of_measurement
) {
this._getHistory();
}
}
}
static get properties() {
return {
_hass: {},
_config: {},
_entity: {},
_line: String,
_min: Number,
_max: Number,
};
}
setConfig(config) {
if (!config.entity || config.entity.split(".")[0] !== "sensor") {
throw new Error("Specify an entity from within the sensor domain.");
}
const cardConfig = {
detail: 1,
icon: false,
height: 100,
hours_to_show: 24,
line_color: "var(--accent-color)",
line_width: 5,
...config,
};
cardConfig.hours_to_show = Number(cardConfig.hours_to_show);
cardConfig.height = Number(cardConfig.height);
cardConfig.line_width = Number(cardConfig.line_width);
cardConfig.detail =
cardConfig.detail === 1 || cardConfig.detail === 2
? cardConfig.detail
: 1;
this._config = cardConfig;
}
shouldUpdate(changedProps) {
const change = changedProps.has("_entity") || changedProps.has("_line");
return change;
}
render({ _config, _entity, _line } = this) {
return html`
${this._style()}
<ha-card @click="${this._handleClick}">
<div class="flex">
<div class="icon">
<ha-icon .icon="${this._computeIcon(_entity)}"></ha-icon>
</div>
<div class="header">
<span class="name">${this._computeName(_entity)}</span>
</div>
</div>
<div class="flex info">
<span id="value">${_entity.state}</span>
<span id="measurement">${this._computeUom(_entity)}</span>
</div>
<div class="graph">
<div>
${
_line
? svg`
<svg width='100%' height='100%' viewBox='0 0 500 ${_config.height}'>
<path d=${_line} fill='none' stroke=${_config.line_color}
stroke-width=${_config.line_width}
stroke-linecap='round' stroke-linejoin='round' />
</svg>`
: ""
}
</div>
</div>
</ha-card>
`;
}
_handleClick() {
this.fire("hass-more-info", { entityId: this._config.entity });
}
_computeIcon(item) {
return this._config.icon || stateIcon(item);
}
_computeName(item) {
return this._config.name || computeStateName(item);
}
_computeUom(item) {
return this._config.unit || item.attributes.unit_of_measurement;
}
_coordinates(history, hours, width, detail = 1) {
history = history.filter((item) => !Number.isNaN(Number(item.state)));
this._min = Math.min.apply(Math, history.map((item) => Number(item.state)));
this._max = Math.max.apply(Math, history.map((item) => Number(item.state)));
const now = new Date().getTime();
const reduce = (res, item, min = false) => {
const age = now - new Date(item.last_changed).getTime();
let key = Math.abs(age / (1000 * 3600) - hours);
if (min) {
key = (key - Math.floor(key)) * 60;
key = (Math.round(key / 10) * 10).toString()[0];
} else {
key = Math.floor(key);
}
if (!res[key]) res[key] = [];
res[key].push(item);
return res;
};
history = history.reduce((res, item) => reduce(res, item), []);
if (detail > 1) {
history = history.map((entry) =>
entry.reduce((res, item) => reduce(res, item, true), [])
);
}
return this._calcPoints(history, hours, width, detail);
}
_calcPoints(history, hours, width, detail = 1) {
const coords = [];
const margin = this._config.line_width;
const height = this._config.height - margin * 4;
width -= margin * 2;
let yRatio = (this._max - this._min) / height;
yRatio = yRatio !== 0 ? yRatio : height;
let xRatio = width / (hours - (detail === 1 ? 1 : 0));
xRatio = isFinite(xRatio) ? xRatio : width;
const getCoords = (item, i, offset = 0, depth = 1) => {
if (depth > 1)
return item.forEach((subItem, index) =>
getCoords(subItem, i, index, depth - 1)
);
const average =
item.reduce((sum, entry) => sum + parseFloat(entry.state), 0) /
item.length;
const x = xRatio * (i + offset / 6) + margin;
const y = height - (average - this._min) / yRatio + margin * 2;
return coords.push([x, y]);
};
history.forEach((item, i) => getCoords(item, i, 0, detail));
if (coords.length === 1) coords[1] = [width + margin, coords[0][1]];
coords.push([width + margin, coords[coords.length - 1][1]]);
return coords;
}
_getPath(coords) {
let next;
let Z;
const X = 0;
const Y = 1;
let path = "";
let last = coords.filter(Boolean)[0];
path += `M ${last[X]},${last[Y]}`;
for (let i = 0; i < coords.length; i++) {
next = coords[i];
Z = this._midPoint(last[X], last[Y], next[X], next[Y]);
path += ` ${Z[X]},${Z[Y]}`;
path += ` Q${next[X]},${next[Y]}`;
last = next;
}
path += ` ${next[X]},${next[Y]}`;
return path;
}
_midPoint(Ax, Ay, Bx, By) {
const Zx = (Ax - Bx) / 2 + Bx;
const Zy = (Ay - By) / 2 + By;
return [Zx, Zy];
}
async _getHistory() {
const endTime = new Date();
const startTime = new Date();
startTime.setHours(endTime.getHours() - this._config.hours_to_show);
const stateHistory = await this._fetchRecent(
this._config.entity,
startTime,
endTime
);
if (stateHistory[0].length < 1) return;
const coords = this._coordinates(
stateHistory[0],
this._config.hours_to_show,
500,
this._config.detail
);
this._line = this._getPath(coords);
}
async _fetchRecent(entityId, startTime, endTime) {
let url = "history/period";
if (startTime) url += "/" + startTime.toISOString();
url += "?filter_entity_id=" + entityId;
if (endTime) url += "&end_time=" + endTime.toISOString();
return await this._hass.callApi("GET", url);
}
getCardSize() {
return 3;
}
_style() {
return html`
<style>
:host {
display: flex;
flex-direction: column;
}
ha-card {
display: flex;
flex-direction: column;
flex: 1;
padding: 16px;
position: relative;
cursor: pointer;
}
.flex {
display: flex;
}
.header {
align-items: center;
display: flex;
min-width: 0;
opacity: 0.8;
position: relative;
}
.name {
display: block;
display: -webkit-box;
font-size: 1.2rem;
font-weight: 500;
max-height: 1.4rem;
margin-top: 2px;
opacity: 0.8;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
word-wrap: break-word;
word-break: break-all;
}
.icon {
color: var(--paper-item-icon-color, #44739e);
display: inline-block;
flex: 0 0 40px;
line-height: 40px;
position: relative;
text-align: center;
width: 40px;
}
.info {
flex-wrap: wrap;
margin: 16px 0 16px 8px;
}
#value {
display: inline-block;
font-size: 2rem;
font-weight: 400;
line-height: 1em;
margin-right: 4px;
}
#measurement {
align-self: flex-end;
display: inline-block;
font-size: 1.3rem;
line-height: 1.2em;
margin-top: 0.1em;
opacity: 0.6;
vertical-align: bottom;
}
.graph {
align-self: flex-end;
margin: auto;
margin-bottom: 0px;
position: relative;
width: 100%;
}
.graph > div {
align-self: flex-end;
margin: auto 8px;
}
</style>
`;
}
}
customElements.define("hui-sensor-card", HuiSensorCard);

View File

@ -0,0 +1,412 @@
import {
html,
svg,
LitElement,
PropertyDeclarations,
PropertyValues,
} from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-spinner/paper-spinner";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import computeStateName from "../../../common/entity/compute_state_name";
import stateIcon from "../../../common/entity/state_icon";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import { fetchRecent } from "../../../data/history";
const midPoint = (
_Ax: number,
_Ay: number,
_Bx: number,
_By: number
): number[] => {
const _Zx = (_Ax - _Bx) / 2 + _Bx;
const _Zy = (_Ay - _By) / 2 + _By;
return [_Zx, _Zy];
};
const getPath = (coords: number[][]): string => {
let next;
let Z;
const X = 0;
const Y = 1;
let path = "";
let last = coords.filter(Boolean)[0];
path += `M ${last[X]},${last[Y]}`;
for (const coord of coords) {
next = coord;
Z = midPoint(last[X], last[Y], next[X], next[Y]);
path += ` ${Z[X]},${Z[Y]}`;
path += ` Q${next[X]},${next[Y]}`;
last = next;
}
path += ` ${next[X]},${next[Y]}`;
return path;
};
const calcPoints = (
history: any,
hours: number,
width: number,
detail: number,
min: number,
max: number
): number[][] => {
const coords = [] as number[][];
const margin = 5;
const height = 80;
width -= 10;
let yRatio = (max - min) / height;
yRatio = yRatio !== 0 ? yRatio : height;
let xRatio = width / (hours - (detail === 1 ? 1 : 0));
xRatio = isFinite(xRatio) ? xRatio : width;
const getCoords = (item, i, offset = 0, depth = 1) => {
if (depth > 1) {
return item.forEach((subItem, index) =>
getCoords(subItem, i, index, depth - 1)
);
}
const average =
item.reduce((sum, entry) => sum + parseFloat(entry.state), 0) /
item.length;
const x = xRatio * (i + offset / 6) + margin;
const y = height - (average - min) / yRatio + margin * 2;
return coords.push([x, y]);
};
history.forEach((item, i) => getCoords(item, i, 0, detail));
if (coords.length === 1) {
coords[1] = [width + margin, coords[0][1]];
}
coords.push([width + margin, coords[coords.length - 1][1]]);
return coords;
};
const coordinates = (
history: any,
hours: number,
width: number,
detail: number
): number[][] => {
history.forEach((item) => (item.state = Number(item.state)));
history = history.filter((item) => !Number.isNaN(item.state));
const min = Math.min.apply(Math, history.map((item) => item.state));
const max = Math.max.apply(Math, history.map((item) => item.state));
const now = new Date().getTime();
const reduce = (res, item, point) => {
const age = now - new Date(item.last_changed).getTime();
let key = Math.abs(age / (1000 * 3600) - hours);
if (point) {
key = (key - Math.floor(key)) * 60;
key = Number((Math.round(key / 10) * 10).toString()[0]);
} else {
key = Math.floor(key);
}
if (!res[key]) {
res[key] = [];
}
res[key].push(item);
return res;
};
history = history.reduce((res, item) => reduce(res, item, false), []);
if (detail > 1) {
history = history.map((entry) =>
entry.reduce((res, item) => reduce(res, item, true), [])
);
}
return calcPoints(history, hours, width, detail, min, max);
};
interface Config extends LovelaceCardConfig {
entity: string;
name?: string;
icon?: string;
graph?: string;
unit?: string;
detail?: number;
theme?: string;
hours_to_show?: number;
}
class HuiSensorCard extends LitElement implements LovelaceCard {
public hass?: HomeAssistant;
private _config?: Config;
private _history?: any;
private _date?: Date;
static get properties(): PropertyDeclarations {
return {
hass: {},
_config: {},
_history: {},
};
}
public setConfig(config: Config): void {
if (!config.entity || config.entity.split(".")[0] !== "sensor") {
throw new Error("Specify an entity from within the sensor domain.");
}
const cardConfig = {
detail: 1,
theme: "default",
hours_to_show: 24,
...config,
};
cardConfig.hours_to_show = Number(cardConfig.hours_to_show);
cardConfig.detail =
cardConfig.detail === 1 || cardConfig.detail === 2
? cardConfig.detail
: 1;
this._config = cardConfig;
}
public getCardSize(): number {
return 3;
}
protected render(): TemplateResult {
if (!this._config || !this.hass) {
return html``;
}
const stateObj = this.hass.states[this._config.entity];
let graph;
if (this._config.graph === "line") {
if (!stateObj.attributes.unit_of_measurement) {
graph = html`
<div class="not-found">
Entity: ${this._config.entity} - Has no Unit of Measurement and
therefore can not display a line graph.
</div>
`;
} else if (!this._history) {
graph = svg`
<svg width="100%" height="100%" viewBox="0 0 500 100"></svg>
`;
} else {
graph = svg`
<svg width="100%" height="100%" viewBox="0 0 500 100">
<path
d="${this._history}"
fill="none"
stroke="var(--accent-color)"
stroke-width="5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
`;
}
} else {
graph = "";
}
return html`
${this.renderStyle()}
<ha-card @click="${this._handleClick}">
${
!stateObj
? html`
<div class="not-found">
Entity not available: ${this._config.entity}
</div>
`
: html`
<div class="flex">
<div class="icon">
<ha-icon
.icon="${this._config.icon || stateIcon(stateObj)}"
></ha-icon>
</div>
<div class="header">
<span class="name"
>${this._config.name || computeStateName(stateObj)}</span
>
</div>
</div>
<div class="flex info">
<span id="value">${stateObj.state}</span>
<span id="measurement"
>${
this._config.unit ||
stateObj.attributes.unit_of_measurement
}</span
>
</div>
<div class="graph"><div>${graph}</div></div>
`
}
</ha-card>
`;
}
protected firstUpdated(): void {
this._date = new Date();
}
protected updated(changedProps: PropertyValues) {
if (!this._config || this._config.graph !== "line" || !this.hass) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes !== this.hass.themes) {
applyThemesOnElement(this, this.hass.themes, this._config!.theme);
}
const minute = 60000;
if (changedProps.has("_config")) {
this._getHistory();
} else if (Date.now() - this._date!.getTime() >= minute) {
this._getHistory();
}
}
private _handleClick(): void {
fireEvent(this, "hass-more-info", { entityId: this._config!.entity });
}
private async _getHistory(): Promise<void> {
const endTime = new Date();
const startTime = new Date();
startTime.setHours(endTime.getHours() - this._config!.hours_to_show!);
const stateHistory = await fetchRecent(
this.hass,
this._config!.entity,
startTime,
endTime
);
if (stateHistory[0].length < 1) {
return;
}
const coords = coordinates(
stateHistory[0],
this._config!.hours_to_show!,
500,
this._config!.detail!
);
this._history = getPath(coords);
this._date = new Date();
}
private renderStyle(): TemplateResult {
return html`
<style>
:host {
display: flex;
flex-direction: column;
}
ha-card {
display: flex;
flex-direction: column;
flex: 1;
padding: 16px;
position: relative;
cursor: pointer;
}
.flex {
display: flex;
}
.header {
align-items: center;
display: flex;
min-width: 0;
opacity: 0.8;
position: relative;
}
.name {
display: block;
display: -webkit-box;
font-size: 1.2rem;
font-weight: 500;
max-height: 1.4rem;
margin-top: 2px;
opacity: 0.8;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
word-wrap: break-word;
word-break: break-all;
}
.icon {
color: var(--paper-item-icon-color, #44739e);
display: inline-block;
flex: 0 0 40px;
line-height: 40px;
position: relative;
text-align: center;
width: 40px;
}
.info {
flex-wrap: wrap;
margin: 16px 0 16px 8px;
}
#value {
display: inline-block;
font-size: 2rem;
font-weight: 400;
line-height: 1em;
margin-right: 4px;
}
#measurement {
align-self: flex-end;
display: inline-block;
font-size: 1.3rem;
line-height: 1.2em;
margin-top: 0.1em;
opacity: 0.6;
vertical-align: bottom;
}
.graph {
align-self: flex-end;
margin: auto;
margin-bottom: 0px;
position: relative;
width: 100%;
}
.graph > div {
align-self: flex-end;
margin: auto 8px;
}
.not-found {
flex: 1;
background-color: yellow;
padding: 8px;
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-sensor-card": HuiSensorCard;
}
}
customElements.define("hui-sensor-card", HuiSensorCard);

View File

@ -9,17 +9,17 @@ import "../../../components/ha-icon";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../types";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import {
fetchItems,
completeItem,
saveEdit,
updateItem,
ShoppingListItem,
clearItems,
addItem,
} from "../../../data/shopping-list";
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
title?: string;
}
@ -256,15 +256,15 @@ class HuiShoppingListCard extends hassLocalizeLitMixin(LitElement)
}
private _completeItem(ev): void {
completeItem(this.hass!, ev.target.itemId, ev.target.checked).catch(() =>
this._fetchData()
);
updateItem(this.hass!, ev.target.itemId, {
complete: ev.target.checked,
}).catch(() => this._fetchData());
}
private _saveEdit(ev): void {
saveEdit(this.hass!, ev.target.itemId, ev.target.value).catch(() =>
this._fetchData()
);
updateItem(this.hass!, ev.target.itemId, {
name: ev.target.value,
}).catch(() => this._fetchData());
ev.target.blur();
}

View File

@ -3,11 +3,12 @@ import { TemplateResult } from "lit-html";
import createCardElement from "../common/create-card-element";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
interface Config extends LovelaceConfig {
cards: LovelaceConfig[];
interface Config extends LovelaceCardConfig {
cards: LovelaceCardConfig[];
}
export abstract class HuiStackCard extends LitElement implements LovelaceCard {

View File

@ -15,7 +15,8 @@ import { hasConfigOrEntityChanged } from "../common/has-changed";
import { roundSliderStyle } from "../../../resources/jquery.roundslider";
import { HomeAssistant, ClimateEntity } from "../../../types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import "../../../components/ha-card";
import "../../../components/ha-icon";
@ -43,9 +44,10 @@ const modeIcons = {
idle: "hass:power-sleep",
};
interface Config extends LovelaceConfig {
interface Config extends LovelaceCardConfig {
entity: string;
theme?: string;
name?: string;
}
function formatTemp(temps: string[]): string {
@ -96,7 +98,8 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
<div id="root">
<div id="thermostat"></div>
<div id="tooltip">
<div class="title">${computeStateName(stateObj)}</div>
<div class="title">${this._config.name ||
computeStateName(stateObj)}</div>
<div class="current-temperature">
<span class="current-temperature-text">
${stateObj.attributes.current_temperature}

View File

@ -20,7 +20,7 @@ export const computeTooltip = (
: config.entity;
}
switch (config.tap_action) {
switch (config.tap_action && config.tap_action.action) {
case "navigate":
tooltip = `Navigate to ${config.navigation_path}`;
break;

View File

@ -1,18 +1,18 @@
import { fireEvent } from "../../../common/dom/fire_event";
import "../cards/hui-alarm-panel-card";
import "../cards/hui-conditional-card.ts";
import "../cards/hui-entities-card.ts";
import "../cards/hui-entity-button-card.ts";
import "../cards/hui-conditional-card";
import "../cards/hui-entities-card";
import "../cards/hui-entity-button-card";
import "../cards/hui-entity-filter-card";
import "../cards/hui-error-card.ts";
import "../cards/hui-glance-card.ts";
import "../cards/hui-error-card";
import "../cards/hui-glance-card";
import "../cards/hui-history-graph-card";
import "../cards/hui-horizontal-stack-card.ts";
import "../cards/hui-iframe-card.ts";
import "../cards/hui-horizontal-stack-card";
import "../cards/hui-iframe-card";
import "../cards/hui-light-card";
import "../cards/hui-map-card";
import "../cards/hui-markdown-card.ts";
import "../cards/hui-markdown-card";
import "../cards/hui-media-control-card";
import "../cards/hui-picture-card";
import "../cards/hui-picture-elements-card";
@ -20,9 +20,9 @@ import "../cards/hui-picture-entity-card";
import "../cards/hui-picture-glance-card";
import "../cards/hui-plant-status-card";
import "../cards/hui-sensor-card";
import "../cards/hui-vertical-stack-card.ts";
import "../cards/hui-vertical-stack-card";
import "../cards/hui-shopping-list-card";
import "../cards/hui-thermostat-card.ts";
import "../cards/hui-thermostat-card";
import "../cards/hui-weather-forecast-card";
import "../cards/hui-gauge-card";

View File

@ -10,6 +10,7 @@ import "../entity-rows/hui-lock-entity-row";
import "../entity-rows/hui-media-player-entity-row";
import "../entity-rows/hui-scene-entity-row";
import "../entity-rows/hui-script-entity-row";
import "../entity-rows/hui-sensor-entity-row";
import "../entity-rows/hui-text-entity-row";
import "../entity-rows/hui-timer-entity-row";
import "../entity-rows/hui-toggle-entity-row";
@ -28,6 +29,7 @@ const SPECIAL_TYPES = new Set([
"weblink",
]);
const DOMAIN_TO_ELEMENT_TYPE = {
alert: "toggle",
automation: "toggle",
climate: "climate",
cover: "cover",
@ -42,6 +44,7 @@ const DOMAIN_TO_ELEMENT_TYPE = {
lock: "lock",
scene: "scene",
script: "script",
sensor: "sensor",
timer: "timer",
switch: "toggle",
vacuum: "toggle",

View File

@ -1,29 +0,0 @@
import { HomeAssistant } from "../../../types";
import { LovelaceConfig } from "../types";
export const getCardConfig = (
hass: HomeAssistant,
cardId: string
): Promise<string> =>
hass.callWS({
type: "lovelace/config/card/get",
card_id: cardId,
});
export const updateCardConfig = (
hass: HomeAssistant,
cardId: string,
config: LovelaceConfig | string,
configFormat: "json" | "yaml"
): Promise<void> =>
hass.callWS({
type: "lovelace/config/card/update",
card_id: cardId,
card_config: config,
format: configFormat,
});
export const migrateConfig = (hass: HomeAssistant): Promise<void> =>
hass.callWS({
type: "lovelace/config/migrate",
});

View File

@ -19,6 +19,8 @@ class LongPress extends HTMLElement implements LongPress {
protected ripple: any;
protected timer: number | undefined;
protected held: boolean;
protected cooldownStart: boolean;
protected cooldownEnd: boolean;
constructor() {
super();
@ -26,6 +28,8 @@ class LongPress extends HTMLElement implements LongPress {
this.ripple = document.createElement("mwc-ripple");
this.timer = undefined;
this.held = false;
this.cooldownStart = false;
this.cooldownEnd = false;
}
public connectedCallback() {
@ -41,7 +45,8 @@ class LongPress extends HTMLElement implements LongPress {
this.ripple.primary = true;
[
isTouch ? "touchcancel" : "mouseout",
"touchcancel",
"mouseout",
"mouseup",
"touchmove",
"mousewheel",
@ -80,6 +85,9 @@ class LongPress extends HTMLElement implements LongPress {
});
const clickStart = (ev: Event) => {
if (this.cooldownStart) {
return;
}
this.held = false;
let x;
let y;
@ -94,30 +102,35 @@ class LongPress extends HTMLElement implements LongPress {
this.startAnimation(x, y);
this.held = true;
}, this.holdTime);
this.cooldownStart = true;
window.setTimeout(() => (this.cooldownStart = false), 100);
};
const clickEnd = () => {
clearTimeout(this.timer);
this.stopAnimation();
if (isTouch && this.timer === undefined) {
const clickEnd = (ev: Event) => {
if (
this.cooldownEnd ||
(ev instanceof TouchEvent && this.timer === undefined)
) {
return;
}
clearTimeout(this.timer);
this.stopAnimation();
this.timer = undefined;
if (this.held) {
element.dispatchEvent(new Event("ha-hold"));
} else {
element.dispatchEvent(new Event("ha-click"));
}
this.cooldownEnd = true;
window.setTimeout(() => (this.cooldownEnd = false), 100);
};
if (isTouch) {
element.addEventListener("touchstart", clickStart, { passive: true });
element.addEventListener("touchend", clickEnd);
element.addEventListener("touchcancel", clickEnd);
} else {
element.addEventListener("mousedown", clickStart, { passive: true });
element.addEventListener("click", clickEnd);
}
element.addEventListener("touchstart", clickStart, { passive: true });
element.addEventListener("touchend", clickEnd);
element.addEventListener("touchcancel", clickEnd);
element.addEventListener("mousedown", clickStart, { passive: true });
element.addEventListener("click", clickEnd);
}
private startAnimation(x: number, y: number) {

View File

@ -1,7 +0,0 @@
import { STATES_OFF } from "../../../../common/const";
import turnOnOffEntity from "./turn-on-off-entity";
export default function toggleEntity(hass, entityId) {
const turnOn = STATES_OFF.includes(hass.states[entityId].state);
turnOnOffEntity(hass, entityId, turnOn);
}

View File

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

View File

@ -1,7 +1,12 @@
import { STATES_OFF } from "../../../../common/const";
import computeDomain from "../../../../common/entity/compute_domain";
import { STATES_OFF } from "../../../../common/const";
import { HomeAssistant } from "../../../../types";
export default function turnOnOffEntities(hass, entityIds, turnOn = true) {
export const turnOnOffEntities = (
hass: HomeAssistant,
entityIds: string[],
turnOn = true
): void => {
const domainsToCall = {};
entityIds.forEach((entityId) => {
if (STATES_OFF.includes(hass.states[entityId].state) === turnOn) {
@ -10,7 +15,9 @@ export default function turnOnOffEntities(hass, entityIds, turnOn = true) {
? stateDomain
: "homeassistant";
if (!(serviceDomain in domainsToCall)) domainsToCall[serviceDomain] = [];
if (!(serviceDomain in domainsToCall)) {
domainsToCall[serviceDomain] = [];
}
domainsToCall[serviceDomain].push(entityId);
}
});
@ -31,4 +38,4 @@ export default function turnOnOffEntities(hass, entityIds, turnOn = true) {
const entities = domainsToCall[domain];
hass.callService(domain, service, { entity_id: entities });
});
}
};

View File

@ -1,6 +1,11 @@
import computeDomain from "../../../../common/entity/compute_domain";
import { HomeAssistant } from "../../../../types";
export default function turnOnOffEntity(hass, entityId, turnOn = true) {
export const turnOnOffEntity = (
hass: HomeAssistant,
entityId: string,
turnOn = true
): Promise<void> => {
const stateDomain = computeDomain(entityId);
const serviceDomain = stateDomain === "group" ? "homeassistant" : stateDomain;
@ -16,5 +21,5 @@ export default function turnOnOffEntity(hass, entityId, turnOn = true) {
service = turnOn ? "turn_on" : "turn_off";
}
hass.callService(serviceDomain, service, { entity_id: entityId });
}
return hass.callService(serviceDomain, service, { entity_id: entityId });
};

View File

@ -0,0 +1,235 @@
import { HomeAssistant, GroupEntity } from "../../../types";
import {
LovelaceConfig,
LovelaceCardConfig,
LovelaceViewConfig,
} from "../../../data/lovelace";
import { HassEntity, HassEntities } from "home-assistant-js-websocket";
import extractViews from "../../../common/entity/extract_views";
import getViewEntities from "../../../common/entity/get_view_entities";
import computeStateName from "../../../common/entity/compute_state_name";
import splitByGroups from "../../../common/entity/split_by_groups";
import computeObjectId from "../../../common/entity/compute_object_id";
import computeStateDomain from "../../../common/entity/compute_state_domain";
import { LocalizeFunc } from "../../../mixins/localize-base-mixin";
import computeDomain from "../../../common/entity/compute_domain";
const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
const DOMAINS_BADGES = [
"binary_sensor",
"device_tracker",
"mailbox",
"sensor",
"sun",
"timer",
];
const HIDE_DOMAIN = new Set(["persistent_notification", "configurator"]);
const computeCards = (
title: string,
states: Array<[string, HassEntity]>
): LovelaceCardConfig[] => {
const cards: LovelaceCardConfig[] = [];
// For entity card
const entities: string[] = [];
for (const [entityId /*, stateObj */] of states) {
const domain = computeDomain(entityId);
if (domain === "alarm_control_panel") {
cards.push({
type: "alarm-panel",
entity: entityId,
});
} else if (domain === "climate") {
cards.push({
type: "thermostat",
entity: entityId,
});
} else if (domain === "media_player") {
cards.push({
type: "media-control",
entity: entityId,
});
} else if (domain === "plant") {
cards.push({
type: "plant-status",
entity: entityId,
});
} else if (domain === "weather") {
cards.push({
type: "weather-forecast",
entity: entityId,
});
} else {
entities.push(entityId);
}
}
if (entities.length > 0) {
cards.unshift({
title,
type: "entities",
entities,
});
}
return cards;
};
const computeDefaultViewStates = (hass: HomeAssistant): HassEntities => {
const states = {};
Object.keys(hass.states).forEach((entityId) => {
const stateObj = hass.states[entityId];
if (
!stateObj.attributes.hidden &&
!HIDE_DOMAIN.has(computeStateDomain(stateObj))
) {
states[entityId] = hass.states[entityId];
}
});
return states;
};
const generateViewConfig = (
localize: LocalizeFunc,
id: string,
title: string | undefined,
icon: string | undefined,
entities: HassEntities,
groupOrders: { [entityId: string]: number }
): LovelaceViewConfig => {
const splitted = splitByGroups(entities);
splitted.groups.sort(
(gr1, gr2) => groupOrders[gr1.entity_id] - groupOrders[gr2.entity_id]
);
const badgeEntities: { [domain: string]: string[] } = {};
const ungroupedEntitites: { [domain: string]: string[] } = {};
// Organize ungrouped entities in badges/ungrouped things
Object.keys(splitted.ungrouped).forEach((entityId) => {
const state = splitted.ungrouped[entityId];
const domain = computeStateDomain(state);
const coll = DOMAINS_BADGES.includes(domain)
? badgeEntities
: ungroupedEntitites;
if (!(domain in coll)) {
coll[domain] = [];
}
coll[domain].push(state.entity_id);
});
let badges: string[] = [];
DOMAINS_BADGES.forEach((domain) => {
if (domain in badgeEntities) {
badges = badges.concat(badgeEntities[domain]);
}
});
let cards: LovelaceCardConfig[] = [];
splitted.groups.forEach((groupEntity) => {
cards = cards.concat(
computeCards(
computeStateName(groupEntity),
groupEntity.attributes.entity_id.map(
(entityId): [string, HassEntity] => [entityId, entities[entityId]]
)
)
);
});
Object.keys(ungroupedEntitites)
.sort()
.forEach((domain) => {
cards = cards.concat(
computeCards(
localize(`domain.${domain}`),
ungroupedEntitites[domain].map(
(entityId): [string, HassEntity] => [entityId, entities[entityId]]
)
)
);
});
return {
id,
title,
icon,
badges,
cards,
};
};
export const generateLovelaceConfig = (
hass: HomeAssistant,
localize: LocalizeFunc
): LovelaceConfig => {
const viewEntities = extractViews(hass.states);
const views = viewEntities.map((viewEntity: GroupEntity) => {
const states = getViewEntities(hass.states, viewEntity);
// In the case of a normal view, we use group order as specified in view
const groupOrders = {};
Object.keys(states).forEach((entityId, idx) => {
groupOrders[entityId] = idx;
});
return generateViewConfig(
localize,
computeObjectId(viewEntity.entity_id),
computeStateName(viewEntity),
viewEntity.attributes.icon,
states,
groupOrders
);
});
let title = hass.config.location_name;
// User can override default view. If they didn't, we will add one
// that contains all entities.
if (
viewEntities.length === 0 ||
viewEntities[0].entity_id !== DEFAULT_VIEW_ENTITY_ID
) {
const states = computeDefaultViewStates(hass);
// In the case of a default view, we want to use the group order attribute
const groupOrders = {};
Object.keys(states).forEach((entityId) => {
const stateObj = states[entityId];
if (stateObj.attributes.order) {
groupOrders[entityId] = stateObj.attributes.order;
}
});
views.unshift(
generateViewConfig(
localize,
"default_view",
"Home",
undefined,
states,
groupOrders
)
);
// Make sure we don't have Home as title and first tab.
if (views.length > 1 && title === "Home") {
title = "Home Assistant";
}
}
return {
_frontendAuto: true,
title,
views,
};
};

View File

@ -0,0 +1,7 @@
const CUSTOM_TYPE_PREFIX = "custom:";
export function getCardElementTag(type: string): string {
return type.startsWith(CUSTOM_TYPE_PREFIX)
? type.substr(CUSTOM_TYPE_PREFIX.length)
: `hui-${type}-card`;
}

View File

@ -1,44 +1,58 @@
import { HomeAssistant } from "../../../types";
import { LovelaceElementConfig } from "../elements/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 "../../../../src/panels/lovelace/common/entity/toggle-entity";
import { ActionConfig } from "../../../data/lovelace";
export const handleClick = (
node: HTMLElement,
hass: HomeAssistant,
config: LovelaceElementConfig,
config: {
entity?: string;
camera_image?: string;
hold_action?: ActionConfig;
tap_action?: ActionConfig;
},
hold: boolean
): void => {
let action = config.tap_action || "more-info";
let actionConfig: ActionConfig | undefined;
if (hold && config.hold_action) {
action = config.hold_action;
actionConfig = config.hold_action;
} else if (!hold && config.tap_action) {
actionConfig = config.tap_action;
}
if (action === "none") {
return;
if (!actionConfig) {
actionConfig = {
action: "more-info",
};
}
switch (action) {
switch (actionConfig.action) {
case "more-info":
fireEvent(node, "hass-more-info", { entityId: config.entity });
if (config.entity || config.camera_image) {
fireEvent(node, "hass-more-info", {
entityId: config.entity ? config.entity! : config.camera_image!,
});
}
break;
case "navigate":
navigate(node, config.navigation_path ? config.navigation_path : "");
if (actionConfig.navigation_path) {
navigate(node, actionConfig.navigation_path);
}
break;
case "toggle":
toggleEntity(hass, config.entity);
if (config.entity) {
toggleEntity(hass, config.entity!);
}
break;
case "call-service": {
if (config.service) {
const [domain, service] = config.service.split(".", 2);
const serviceData = {
entity_id: config.entity,
...config.service_data,
};
hass.callService(domain, service, serviceData);
if (!actionConfig.service) {
return;
}
const [domain, service] = actionConfig.service.split(".", 2);
hass.callService(domain, service, actionConfig.service_data);
}
}
};

View File

@ -1,38 +0,0 @@
// Parse array of entity objects from config
import isValidEntityId from "../../../common/entity/valid_entity_id";
export default function processConfigEntities(entities) {
if (!entities || !Array.isArray(entities)) {
throw new Error("Entities need to be an array");
}
return entities.map((entityConf, index) => {
if (
typeof entityConf === "object" &&
!Array.isArray(entityConf) &&
entityConf.type
) {
return entityConf;
}
if (typeof entityConf === "string") {
entityConf = { entity: entityConf };
} else if (typeof entityConf === "object" && !Array.isArray(entityConf)) {
if (!entityConf.entity) {
throw new Error(
`Entity object at position ${index} is missing entity field.`
);
}
} else {
throw new Error(`Invalid entity specified at position ${index}.`);
}
if (!isValidEntityId(entityConf.entity)) {
throw new Error(
`Invalid entity ID at position ${index}: ${entityConf.entity}`
);
}
return entityConf;
});
}

View File

@ -0,0 +1,47 @@
// Parse array of entity objects from config
import isValidEntityId from "../../../common/entity/valid_entity_id";
import { EntityConfig } from "../entity-rows/types";
export const processConfigEntities = <T extends EntityConfig>(
entities: Array<T | string>
): T[] => {
if (!entities || !Array.isArray(entities)) {
throw new Error("Entities need to be an array");
}
return entities.map(
(entityConf, index): T => {
if (
typeof entityConf === "object" &&
!Array.isArray(entityConf) &&
entityConf.type
) {
return entityConf;
}
let config: T;
if (typeof entityConf === "string") {
// tslint:disable-next-line:no-object-literal-type-assertion
config = { entity: entityConf } as T;
} else if (typeof entityConf === "object" && !Array.isArray(entityConf)) {
if (!entityConf.entity) {
throw new Error(
`Entity object at position ${index} is missing entity field.`
);
}
config = entityConf as T;
} else {
throw new Error(`Invalid entity specified at position ${index}.`);
}
if (!isValidEntityId(config.entity)) {
throw new Error(
`Invalid entity ID at position ${index}: ${config.entity}`
);
}
return config;
}
);
};

View File

@ -0,0 +1,9 @@
export function isEntityId(value: any): string | boolean {
if (typeof value !== "string") {
return "entity id should be a string";
}
if (!value.includes(".")) {
return "entity id should be in the format 'domain.entity'";
}
return true;
}

View File

@ -0,0 +1,9 @@
export function isIcon(value: any): string | boolean {
if (typeof value !== "string") {
return "icon should be a string";
}
if (!value.includes(":")) {
return "icon should be in the format 'mdi:icon'";
}
return true;
}

View File

@ -0,0 +1,10 @@
import { superstruct } from "superstruct";
import { isEntityId } from "./is-entity-id";
import { isIcon } from "./is-icon";
export const struct = superstruct({
types: {
"entity-id": isEntityId,
icon: isIcon,
},
});

View File

@ -1,31 +1,33 @@
import "@polymer/paper-button/paper-button";
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import "@polymer/paper-button/paper-button";
import { fireEvent } from "../../../common/dom/fire_event";
import { showEditCardDialog } from "../editor/hui-dialog-edit-card";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { confDeleteCard } from "../editor/delete-card";
import { HomeAssistant } from "../../../types";
import { LovelaceConfig } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
let registeredDialog = false;
declare global {
// for fire event
interface HASSDomEvents {
"show-edit-card": {
cardConfig?: LovelaceCardConfig;
viewId?: string | number;
add: boolean;
reloadLovelace: () => void;
};
}
}
export class HuiCardOptions extends LitElement {
public cardConfig?: LovelaceConfig;
export class HuiCardOptions extends hassLocalizeLitMixin(LitElement) {
public cardConfig?: LovelaceCardConfig;
protected hass?: HomeAssistant;
static get properties(): PropertyDeclarations {
return { hass: {} };
}
public connectedCallback() {
super.connectedCallback();
if (!registeredDialog) {
registeredDialog = true;
fireEvent(this, "register-dialog", {
dialogShowEvent: "show-edit-card",
dialogTag: "hui-dialog-edit-card",
dialogImport: () => import("../editor/hui-dialog-edit-card"),
});
}
}
protected render() {
return html`
<style>
@ -42,18 +44,46 @@ export class HuiCardOptions extends LitElement {
color: var(--primary-color);
font-weight: 500;
}
paper-button.warning:not([disabled]) {
color: var(--google-red-500);
}
</style>
<slot></slot>
<div><paper-button @click="${this._editCard}">EDIT</paper-button></div>
<div>
<paper-button class="warning" @click="${this._deleteCard}"
>${
this.localize("ui.panel.lovelace.editor.edit_card.delete")
}</paper-button
><paper-button @click="${this._editCard}"
>${
this.localize("ui.panel.lovelace.editor.edit_card.edit")
}</paper-button
>
</div>
`;
}
private _editCard() {
fireEvent(this, "show-edit-card", {
hass: this.hass,
private _editCard(): void {
if (!this.cardConfig) {
return;
}
showEditCardDialog(this, {
cardConfig: this.cardConfig,
add: false,
reloadLovelace: () => fireEvent(this, "config-refresh"),
});
}
private _deleteCard(): void {
if (!this.cardConfig) {
return;
}
if (!this.cardConfig.id) {
this._editCard();
return;
}
confDeleteCard(this.hass!, this.cardConfig.id, () =>
fireEvent(this, "config-refresh")
);
}
}
declare global {

View File

@ -1,60 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/paper-toggle-button/paper-toggle-button";
import { DOMAINS_TOGGLE } from "../../../common/const";
import turnOnOffEntities from "../common/entity/turn-on-off-entities";
class HuiEntitiesToggle extends PolymerElement {
static get template() {
return html`
<style>
:host {
width: 38px;
display: block;
}
paper-toggle-button {
cursor: pointer;
--paper-toggle-button-label-spacing: 0;
padding: 13px 5px;
margin: -4px -5px;
}
</style>
<template is="dom-if" if="[[_toggleEntities.length]]">
<paper-toggle-button
checked="[[_computeIsChecked(hass, _toggleEntities)]]"
on-change="_callService"
></paper-toggle-button>
</template>
`;
}
static get properties() {
return {
hass: Object,
entities: Array,
_toggleEntities: {
type: Array,
computed: "_computeToggleEntities(hass, entities)",
},
};
}
_computeToggleEntities(hass, entityIds) {
return entityIds.filter(
(entityId) =>
entityId in hass.states && DOMAINS_TOGGLE.has(entityId.split(".", 1)[0])
);
}
_computeIsChecked(hass, entityIds) {
return entityIds.some((entityId) => hass.states[entityId].state === "on");
}
_callService(ev) {
const turnOn = ev.target.checked;
turnOnOffEntities(this.hass, this._toggleEntities, turnOn);
}
}
customElements.define("hui-entities-toggle", HuiEntitiesToggle);

View File

@ -0,0 +1,84 @@
import {
html,
LitElement,
PropertyDeclarations,
PropertyValues,
} from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { PaperToggleButtonElement } from "@polymer/paper-toggle-button/paper-toggle-button";
import { DOMAINS_TOGGLE } from "../../../common/const";
import { turnOnOffEntities } from "../common/entity/turn-on-off-entities";
import { HomeAssistant } from "../../../types";
class HuiEntitiesToggle extends LitElement {
public entities?: string[];
protected hass?: HomeAssistant;
private _toggleEntities?: string[];
static get properties(): PropertyDeclarations {
return {
hass: {},
entities: {},
_toggleEntities: {},
};
}
public updated(changedProperties: PropertyValues) {
if (changedProperties.has("entities")) {
this._toggleEntities = this.entities!.filter(
(entityId) =>
entityId in this.hass!.states &&
DOMAINS_TOGGLE.has(entityId.split(".", 1)[0])
);
}
}
protected render(): TemplateResult {
if (!this._toggleEntities) {
return html``;
}
return html`
${this.renderStyle()}
<paper-toggle-button
?checked="${
this._toggleEntities!.some(
(entityId) => this.hass!.states[entityId].state === "on"
)
}"
@change="${this._callService}"
></paper-toggle-button>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
:host {
width: 38px;
display: block;
}
paper-toggle-button {
cursor: pointer;
--paper-toggle-button-label-spacing: 0;
padding: 13px 5px;
margin: -4px -5px;
}
</style>
`;
}
private _callService(ev: MouseEvent): void {
const turnOn = (ev.target as PaperToggleButtonElement).checked;
turnOnOffEntities(this.hass!, this._toggleEntities!, turnOn!);
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-entities-toggle": HuiEntitiesToggle;
}
}
customElements.define("hui-entities-toggle", HuiEntitiesToggle);

View File

@ -42,16 +42,23 @@ export class HuiEntityEditor extends LitElement {
`;
})
}
<ha-entity-picker
.hass="${this.hass}"
@change="${this._addEntity}"
></ha-entity-picker>
</div>
<paper-button noink raised @click="${this._addEntity}"
>Add Entity</paper-button
>
`;
}
private _addEntity() {
const newConfigEntities = this.entities!.concat({ entity: "" });
private _addEntity(ev: Event): void {
const target = ev.target! as EditorTarget;
if (target.value === "") {
return;
}
const newConfigEntities = this.entities!.concat({
entity: target.value as string,
});
target.value = "";
fireEvent(this, "entities-changed", { entities: newConfigEntities });
}

View File

@ -3,9 +3,20 @@ import "@polymer/paper-button/paper-button";
import { TemplateResult } from "lit-html";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
declare global {
// for fire event
interface HASSDomEvents {
"theme-changed": undefined;
}
// for add event listener
interface HTMLElementEventMap {
"theme-changed": HASSDomEvent<undefined>;
}
}
export class HuiThemeSelectionEditor extends hassLocalizeLitMixin(LitElement) {
public value?: string;
protected hass?: HomeAssistant;

View File

@ -0,0 +1,130 @@
import {
html,
LitElement,
PropertyDeclarations,
PropertyValues,
} from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { HomeAssistant } from "../../../types";
import format_date from "../../../common/datetime/format_date";
import format_date_time from "../../../common/datetime/format_date_time";
import format_time from "../../../common/datetime/format_time";
import relativeTime from "../../../common/datetime/relative_time";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
const FORMATS: { [key: string]: (ts: Date, lang: string) => string } = {
date: format_date,
datetime: format_date_time,
time: format_time,
};
const INTERVAL_FORMAT = ["relative", "total"];
class HuiTimestampDisplay extends hassLocalizeLitMixin(LitElement) {
public hass?: HomeAssistant;
public ts?: Date;
public format?: "relative" | "total" | "date" | "datetime" | "time";
private _relative?: string;
private _connected?: boolean;
private _interval?: number;
static get properties(): PropertyDeclarations {
return {
ts: {},
hass: {},
format: {},
_relative: {},
};
}
public connectedCallback() {
super.connectedCallback();
this._connected = true;
this._startInterval();
}
public disconnectedCallback() {
super.disconnectedCallback();
this._connected = false;
this._clearInterval();
}
protected render(): TemplateResult {
if (!this.ts || !this.hass) {
return html``;
}
if (isNaN(this.ts.getTime())) {
return html`
Invalid date
`;
}
const format = this._format;
if (INTERVAL_FORMAT.includes(format)) {
return html`
${this._relative}
`;
} else if (format in FORMATS) {
return html`
${FORMATS[format](this.ts, this.hass.language)}
`;
} else {
return html`
Invalid format
`;
}
}
protected updated(changedProperties: PropertyValues) {
if (!changedProperties.has("format") || !this._connected) {
return;
}
if (INTERVAL_FORMAT.includes("relative")) {
this._startInterval();
} else {
this._clearInterval();
}
}
private get _format() {
return this.format || "relative";
}
private _startInterval() {
this._clearInterval();
if (this._connected && INTERVAL_FORMAT.includes(this._format)) {
this._updateRelative();
this._interval = window.setInterval(() => this._updateRelative(), 1000);
}
}
private _clearInterval() {
if (this._interval) {
clearInterval(this._interval);
this._interval = undefined;
}
}
private _updateRelative() {
if (this.ts && this.localize) {
this._relative =
this._format === "relative"
? relativeTime(this.ts, this.localize)
: (this._relative = relativeTime(new Date(), this.localize, {
compareTime: this.ts,
includeTense: false,
}));
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-timestamp-display": HuiTimestampDisplay;
}
}
customElements.define("hui-timestamp-display", HuiTimestampDisplay);

View File

@ -0,0 +1,16 @@
import { html } from "@polymer/lit-element";
export const configElementStyle = html`
<style>
paper-toggle-button {
padding-top: 16px;
}
.side-by-side {
display: flex;
}
.side-by-side > * {
flex: 1;
padding-right: 4px;
}
</style>
`;

View File

@ -1,17 +1,20 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-checkbox/paper-checkbox";
import { struct } from "../../common/structs/struct";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-toggle-button/paper-toggle-button";
import { processEditorEntities } from "../process-editor-entities";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config, ConfigEntity } from "../../cards/hui-entities-card";
import { configElementStyle } from "./config-elements-style";
import "../../../../components/entity/state-badge";
import "../../components/hui-theme-select-editor";
@ -19,12 +22,26 @@ import "../../components/hui-entity-editor";
import "../../../../components/ha-card";
import "../../../../components/ha-icon";
const entitiesConfigStruct = struct.union([
{
entity: "entity-id",
name: "string?",
icon: "icon?",
},
"entity-id",
]);
const cardConfigStruct = struct({
type: "string",
id: "string|number",
title: "string|number?",
theme: "string?",
show_header_toggle: "boolean?",
entities: [entitiesConfigStruct],
});
export class HuiEntitiesCardEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
private _configEntities?: ConfigEntity[];
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {}, _configEntities: {} };
}
@ -37,7 +54,13 @@ export class HuiEntitiesCardEditor extends hassLocalizeLitMixin(LitElement)
return this._config!.theme || "Backend-selected";
}
public hass?: HomeAssistant;
private _config?: Config;
private _configEntities?: ConfigEntity[];
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = { type: "entities", ...config };
this._configEntities = processEditorEntities(config.entities);
}
@ -48,30 +71,32 @@ export class HuiEntitiesCardEditor extends hassLocalizeLitMixin(LitElement)
}
return html`
${this.renderStyle()}
<paper-input
label="Title"
value="${this._title}"
.configValue="${"title"}"
@value-changed="${this._valueChanged}"
></paper-input>
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
${configElementStyle}
<div class="card-config">
<paper-input
label="Title"
value="${this._title}"
.configValue="${"title"}"
@value-changed="${this._valueChanged}"
></paper-input>
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
<paper-toggle-button
?checked="${this._config!.show_header_toggle !== false}"
.configValue="${"show_header_toggle"}"
@change="${this._valueChanged}"
>Show Header Toggle?</paper-toggle-button
>
</div>
<hui-entity-editor
.hass="${this.hass}"
.entities="${this._configEntities}"
@entities-changed="${this._valueChanged}"
></hui-entity-editor>
<paper-checkbox
?checked="${this._config!.show_header_toggle !== false}"
.configValue="${"show_header_toggle"}"
@change="${this._valueChanged}"
>Show Header Toggle?</paper-checkbox
>
`;
}
@ -102,17 +127,6 @@ export class HuiEntitiesCardEditor extends hassLocalizeLitMixin(LitElement)
fireEvent(this, "config-changed", { config: this._config });
}
private renderStyle(): TemplateResult {
return html`
<style>
paper-checkbox {
display: block;
padding-top: 16px;
}
</style>
`;
}
}
declare global {

View File

@ -1,9 +1,10 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-checkbox/paper-checkbox";
import { struct } from "../../common/structs/struct";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-toggle-button/paper-toggle-button";
import { processEditorEntities } from "../process-editor-entities";
import { EntitiesEditorEvent, EditorTarget } from "../types";
@ -12,6 +13,7 @@ import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config, ConfigEntity } from "../../cards/hui-glance-card";
import { configElementStyle } from "./config-elements-style";
import "../../../../components/entity/state-badge";
import "../../components/hui-theme-select-editor";
@ -19,6 +21,26 @@ import "../../components/hui-entity-editor";
import "../../../../components/ha-card";
import "../../../../components/ha-icon";
const entitiesConfigStruct = struct.union([
{
entity: "entity-id",
name: "string?",
icon: "icon?",
},
"entity-id",
]);
const cardConfigStruct = struct({
type: "string",
id: "string|number",
title: "string|number?",
theme: "string?",
columns: "number?",
show_name: "boolean?",
show_state: "boolean?",
entities: [entitiesConfigStruct],
});
export class HuiGlanceCardEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
@ -26,6 +48,8 @@ export class HuiGlanceCardEditor extends hassLocalizeLitMixin(LitElement)
private _configEntities?: ConfigEntity[];
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = { type: "glance", ...config };
this._configEntities = processEditorEntities(config.entities);
}
@ -52,42 +76,48 @@ export class HuiGlanceCardEditor extends hassLocalizeLitMixin(LitElement)
}
return html`
${this.renderStyle()}
<paper-input
label="Title"
value="${this._title}"
.configValue="${"title"}"
@value-changed="${this._valueChanged}"
></paper-input>
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
<paper-input
label="Columns"
value="${this._columns}"
.configValue="${"columns"}"
@value-changed="${this._valueChanged}"
></paper-input>
${configElementStyle}
<div class="card-config">
<paper-input
label="Title"
value="${this._title}"
.configValue="${"title"}"
@value-changed="${this._valueChanged}"
></paper-input>
<div class="side-by-side">
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
<paper-input
label="Columns"
value="${this._columns}"
.configValue="${"columns"}"
@value-changed="${this._valueChanged}"
></paper-input>
</div>
<div class="side-by-side">
<paper-toggle-button
?checked="${this._config!.show_name !== false}"
.configValue="${"show_name"}"
@change="${this._valueChanged}"
>Show Entity's Name?</paper-toggle-button
>
<paper-toggle-button
?checked="${this._config!.show_state !== false}"
.configValue="${"show_state"}"
@change="${this._valueChanged}"
>Show Entity's State Text?</paper-toggle-button
>
</div>
</div>
<hui-entity-editor
.hass="${this.hass}"
.entities="${this._configEntities}"
@entities-changed="${this._valueChanged}"
></hui-entity-editor>
<paper-checkbox
?checked="${this._config!.show_name !== false}"
.configValue="${"show_name"}"
@change="${this._valueChanged}"
>Show Entity's Name?</paper-checkbox
>
<paper-checkbox
?checked="${this._config!.show_state !== false}"
.configValue="${"show_state"}"
@change="${this._valueChanged}"
>Show Entity's State Text?</paper-checkbox
>
`;
}
@ -117,17 +147,6 @@ export class HuiGlanceCardEditor extends hassLocalizeLitMixin(LitElement)
}
fireEvent(this, "config-changed", { config: this._config });
}
private renderStyle(): TemplateResult {
return html`
<style>
paper-checkbox {
display: block;
padding-top: 16px;
}
</style>
`;
}
}
declare global {

View File

@ -0,0 +1,128 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-input";
import { EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { configElementStyle } from "./config-elements-style";
import "../../components/hui-theme-select-editor";
import { LovelaceViewConfig } from "../../../../data/lovelace";
declare global {
interface HASSDomEvents {
"view-config-changed": {
config: LovelaceViewConfig;
};
}
}
export class HuiViewEditor extends hassLocalizeLitMixin(LitElement) {
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {} };
}
get _id(): string {
if (!this._config) {
return "";
}
return this._config.id || "";
}
get _title(): string {
if (!this._config) {
return "";
}
return this._config.title || "";
}
get _icon(): string {
if (!this._config) {
return "";
}
return this._config.icon || "";
}
get _theme(): string {
if (!this._config) {
return "";
}
return this._config.theme || "Backend-selected";
}
public hass?: HomeAssistant;
private _config?: LovelaceViewConfig;
set config(config: LovelaceViewConfig) {
this._config = config;
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${configElementStyle}
<div class="card-config">
<paper-input
label="ID"
value="${this._id}"
.configValue="${"id"}"
@value-changed="${this._valueChanged}"
></paper-input>
<paper-input
label="Title"
value="${this._title}"
.configValue="${"title"}"
@value-changed="${this._valueChanged}"
></paper-input>
<paper-input
label="Icon"
value="${this._icon}"
.configValue="${"icon"}"
@value-changed="${this._valueChanged}"
></paper-input>
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
</div>
`;
}
private _valueChanged(ev: Event): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.currentTarget! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
let newConfig;
if (target.configValue) {
newConfig = {
...this._config,
[target.configValue]: target.value,
};
}
fireEvent(this, "view-config-changed", { config: newConfig });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-view-editor": HuiViewEditor;
}
}
customElements.define("hui-view-editor", HuiViewEditor);

View File

@ -0,0 +1,18 @@
import { deleteCard } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
export async function confDeleteCard(
hass: HomeAssistant,
cardId: string,
reloadLovelace: () => void
): Promise<void> {
if (!confirm("Are you sure you want to delete this card?")) {
return;
}
try {
await deleteCard(hass, String(cardId));
reloadLovelace();
} catch (err) {
alert(`Deleting failed: ${err.message}`);
}
}

View File

@ -0,0 +1,18 @@
import { deleteView } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
export async function confDeleteView(
hass: HomeAssistant,
viewId: string,
reloadLovelace: () => void
): Promise<void> {
if (!confirm("Are you sure you want to delete this view?")) {
return;
}
try {
await deleteView(hass, String(viewId));
reloadLovelace();
} catch (err) {
alert(`Deleting failed: ${err.message}`);
}
}

View File

@ -0,0 +1,112 @@
import { html, LitElement } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-button/paper-button";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { getCardElementTag } from "../common/get-card-element-tag";
import { CardPickTarget } from "./types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { uid } from "../../../common/util/uid";
declare global {
interface HASSDomEvents {
"card-picked": {
config: LovelaceCardConfig;
};
}
}
const cards = [
{ name: "Alarm panel", type: "alarm-panel" },
{ name: "Conditional", type: "conditional" },
{ name: "Entities", type: "entities" },
{ name: "Entity Button", type: "entity-button" },
{ name: "Entity Filter", type: "entity-filter" },
{ name: "Gauge", type: "gauge" },
{ name: "Glance", type: "glance" },
{ name: "History Graph", type: "history-graph" },
{ name: "Horizontal Stack", type: "horizontal-graph" },
{ name: "iFrame", type: "iframe" },
{ name: "Light", type: "light" },
{ name: "Map", type: "map" },
{ name: "Markdown", type: "markdown" },
{ name: "Media Control", type: "media-control" },
{ name: "Picture", type: "picture" },
{ name: "Picture Elements", type: "picture-elements" },
{ name: "Picture Entity", type: "picture-entity" },
{ name: "Picture Glance", type: "picture-glance" },
{ name: "Plant Status", type: "plant-status" },
{ name: "Sensor", type: "sensor" },
{ name: "Shopping List", type: "shopping-list" },
{ name: "Thermostat", type: "thermostat" },
{ name: "Vertical Stack", type: "vertical-stack" },
{ name: "Weather Forecast", type: "weather-forecast" },
];
export class HuiCardPicker extends hassLocalizeLitMixin(LitElement) {
protected hass?: HomeAssistant;
protected render(): TemplateResult {
return html`
${this.renderStyle()}
<h3>${this.localize("ui.panel.lovelace.editor.edit_card.pick_card")}</h3>
<div class="cards-container">
${
cards.map((card) => {
return html`
<paper-button
raised
@click="${this._cardPicked}"
.type="${card.type}"
>${card.name}</paper-button
>
`;
})
}
</div>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
.cards-container {
display: flex;
flex-wrap: wrap;
margin-bottom: 10px;
}
.cards-container paper-button {
flex: 1 0 25%;
}
</style>
`;
}
private _cardPicked(ev: Event): void {
const type = (ev.currentTarget! as CardPickTarget).type;
const tag = getCardElementTag(type);
const elClass = customElements.get(tag);
let config: LovelaceCardConfig = { type, id: uid() };
if (elClass && elClass.getStubConfig) {
const cardConfig = elClass.getStubConfig(this.hass);
config = { ...config, ...cardConfig };
}
fireEvent(this, "card-picked", {
config,
});
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-card-picker": HuiCardPicker;
}
}
customElements.define("hui-card-picker", HuiCardPicker);

View File

@ -3,10 +3,10 @@ import "@polymer/paper-input/paper-textarea";
import createCardElement from "../common/create-card-element";
import createErrorCardConfig from "../common/create-error-card-config";
import { HomeAssistant } from "../../../types";
import { LovelaceCard, LovelaceConfig } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { LovelaceCard } from "../types";
import { ConfigError } from "./types";
const CUSTOM_TYPE_PREFIX = "custom:";
import { getCardElementTag } from "../common/get-card-element-tag";
export class HuiCardPreview extends HTMLElement {
private _hass?: HomeAssistant;
@ -28,7 +28,7 @@ export class HuiCardPreview extends HTMLElement {
this._createCard(configValue);
}
set config(configValue: LovelaceConfig) {
set config(configValue: LovelaceCardConfig) {
if (!configValue) {
return;
}
@ -38,18 +38,20 @@ export class HuiCardPreview extends HTMLElement {
return;
}
const tag = configValue.type.startsWith(CUSTOM_TYPE_PREFIX)
? configValue.type.substr(CUSTOM_TYPE_PREFIX.length)
: `hui-${configValue.type}-card`;
const tag = getCardElementTag(configValue.type);
if (tag.toUpperCase() === this._element.tagName) {
this._element.setConfig(configValue);
try {
this._element.setConfig(configValue);
} catch (err) {
this._createCard(createErrorCardConfig(err.message, configValue));
}
} else {
this._createCard(configValue);
}
}
private _createCard(configValue: LovelaceConfig): void {
private _createCard(configValue: LovelaceCardConfig): void {
if (this._element) {
this.removeChild(this._element);
}

View File

@ -2,51 +2,106 @@ import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { HomeAssistant } from "../../../types";
import { LovelaceConfig } from "../types";
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
import { LovelaceCardConfig } from "../../../data/lovelace";
import "./hui-edit-card";
import "./hui-migrate-config";
declare global {
// for fire event
interface HASSDomEvents {
"reload-lovelace": undefined;
"show-edit-card": EditCardDialogParams;
}
// for add event listener
interface HTMLElementEventMap {
"reload-lovelace": HASSDomEvent<undefined>;
}
}
let registeredDialog = false;
const dialogShowEvent = "show-edit-card";
const dialogTag = "hui-dialog-edit-card";
export interface EditCardDialogParams {
cardConfig?: LovelaceCardConfig;
viewId?: string | number;
add: boolean;
reloadLovelace: () => void;
}
const registerEditCardDialog = (element: HTMLElement) =>
fireEvent(element, "register-dialog", {
dialogShowEvent,
dialogTag,
dialogImport: () => import("./hui-dialog-edit-card"),
});
export const showEditCardDialog = (
element: HTMLElement,
editCardDialogParams: EditCardDialogParams
) => {
if (!registeredDialog) {
registeredDialog = true;
registerEditCardDialog(element);
}
fireEvent(element, dialogShowEvent, editCardDialogParams);
};
export class HuiDialogEditCard extends LitElement {
protected _hass?: HomeAssistant;
private _cardConfig?: LovelaceConfig;
private _reloadLovelace?: () => void;
protected hass?: HomeAssistant;
private _params?: EditCardDialogParams;
static get properties(): PropertyDeclarations {
return {
_hass: {},
_cardConfig: {},
hass: {},
_params: {},
};
}
public async showDialog({ hass, cardConfig, reloadLovelace }): Promise<void> {
this._hass = hass;
this._cardConfig = cardConfig;
this._reloadLovelace = reloadLovelace;
public async showDialog(params: EditCardDialogParams): Promise<void> {
this._params = params;
await this.updateComplete;
(this.shadowRoot!.children[0] as any).showDialog();
}
protected render(): TemplateResult {
if (!this._params) {
return html``;
}
if (
(!this._params.add &&
this._params.cardConfig &&
!this._params.cardConfig.id) ||
(this._params.add && !this._params.viewId)
) {
return html`
<hui-migrate-config
.hass="${this.hass}"
@reload-lovelace="${this._params.reloadLovelace}"
></hui-migrate-config>
`;
}
return html`
${
this._cardConfig!.id
? html`
<hui-edit-card
.cardConfig="${this._cardConfig}"
.hass="${this._hass}"
@reload-lovelace="${this._reloadLovelace}"
>
</hui-edit-card>
`
: html`
<hui-migrate-config
.hass="${this._hass}"
@reload-lovelace="${this._reloadLovelace}"
></hui-migrate-config>
`
}
<hui-edit-card
.hass="${this.hass}"
.viewId="${this._params.viewId}"
.cardConfig="${this._params.cardConfig}"
@reload-lovelace="${this._params.reloadLovelace}"
@cancel-edit-card="${this._cancel}"
>
</hui-edit-card>
`;
}
private _cancel() {
this._params = {
add: false,
reloadLovelace: () => {
return;
},
};
}
}
declare global {
@ -55,4 +110,4 @@ declare global {
}
}
customElements.define("hui-dialog-edit-card", HuiDialogEditCard);
customElements.define(dialogTag, HuiDialogEditCard);

View File

@ -0,0 +1,101 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { HomeAssistant } from "../../../types";
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
import { LovelaceViewConfig } from "../../../data/lovelace";
import "./hui-edit-view";
import "./hui-migrate-config";
declare global {
// for fire event
interface HASSDomEvents {
"reload-lovelace": undefined;
"show-edit-view": EditViewDialogParams;
}
// for add event listener
interface HTMLElementEventMap {
"reload-lovelace": HASSDomEvent<undefined>;
}
}
let registeredDialog = false;
const dialogShowEvent = "show-edit-view";
const dialogTag = "hui-dialog-edit-view";
export interface EditViewDialogParams {
viewConfig?: LovelaceViewConfig;
add?: boolean;
reloadLovelace: () => void;
}
const registerEditViewDialog = (element: HTMLElement) =>
fireEvent(element, "register-dialog", {
dialogShowEvent,
dialogTag,
dialogImport: () => import("./hui-dialog-edit-view"),
});
export const showEditViewDialog = (
element: HTMLElement,
editViewDialogParams: EditViewDialogParams
) => {
if (!registeredDialog) {
registeredDialog = true;
registerEditViewDialog(element);
}
fireEvent(element, dialogShowEvent, editViewDialogParams);
};
export class HuiDialogEditView extends LitElement {
protected hass?: HomeAssistant;
private _params?: EditViewDialogParams;
static get properties(): PropertyDeclarations {
return {
hass: {},
_params: {},
};
}
public async showDialog(params: EditViewDialogParams): Promise<void> {
this._params = params;
await this.updateComplete;
(this.shadowRoot!.children[0] as any).showDialog();
}
protected render(): TemplateResult {
if (!this._params) {
return html``;
}
if (
!this._params.add &&
this._params.viewConfig &&
!this._params.viewConfig.id
) {
return html`
<hui-migrate-config
.hass="${this.hass}"
@reload-lovelace="${this._params.reloadLovelace}"
></hui-migrate-config>
`;
}
return html`
<hui-edit-view
.hass="${this.hass}"
.viewConfig="${this._params.viewConfig}"
.add="${this._params.add}"
.reloadLovelace="${this._params.reloadLovelace}"
>
</hui-edit-view>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-dialog-edit-view": HuiDialogEditView;
}
}
customElements.define(dialogTag, HuiDialogEditView);

View File

@ -0,0 +1,160 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-spinner/paper-spinner";
import "@polymer/paper-dialog/paper-dialog";
// This is not a duplicate import, one is for types, one is for element.
// tslint:disable-next-line
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-button/paper-button";
import { HomeAssistant } from "../../../types";
import {
saveConfig,
migrateConfig,
LovelaceConfig,
} from "../../../data/lovelace";
import { fireEvent } from "../../../common/dom/fire_event";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
declare global {
// for fire event
interface HASSDomEvents {
"show-save-config": SaveDialogParams;
}
}
const dialogShowEvent = "show-save-config";
const dialogTag = "hui-dialog-save-config";
export interface SaveDialogParams {
config: LovelaceConfig;
reloadLovelace: () => void;
}
export const registerSaveDialog = (element: HTMLElement) =>
fireEvent(element, "register-dialog", {
dialogShowEvent,
dialogTag,
dialogImport: () => import("./hui-dialog-save-config"),
});
export const showSaveDialog = (
element: HTMLElement,
saveDialogParams: SaveDialogParams
) => fireEvent(element, dialogShowEvent, saveDialogParams);
export class HuiSaveConfig extends hassLocalizeLitMixin(LitElement) {
protected hass?: HomeAssistant;
private _params?: SaveDialogParams;
private _saving: boolean;
static get properties(): PropertyDeclarations {
return {
hass: {},
_params: {},
_saving: {},
};
}
protected constructor() {
super();
this._saving = false;
}
public async showDialog(params: SaveDialogParams): Promise<void> {
this._params = params;
await this.updateComplete;
this._dialog.open();
}
private get _dialog(): PaperDialogElement {
return this.shadowRoot!.querySelector("paper-dialog")!;
}
protected render(): TemplateResult {
return html`
${this.renderStyle()}
<paper-dialog with-backdrop>
<h2>${this.localize("ui.panel.lovelace.editor.save_config.header")}</h2>
<paper-dialog-scrollable>
<p>${this.localize("ui.panel.lovelace.editor.save_config.para")}</p>
<p>
${this.localize("ui.panel.lovelace.editor.save_config.para_sure")}
</p>
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
<paper-button @click="${this._closeDialog}"
>${
this.localize("ui.panel.lovelace.editor.save_config.cancel")
}</paper-button
>
<paper-button
?disabled="${this._saving}"
@click="${this._saveConfig}"
>
<paper-spinner
?active="${this._saving}"
alt="Saving"
></paper-spinner>
${
this.localize("ui.panel.lovelace.editor.save_config.save")
}</paper-button
>
</div>
</paper-dialog>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
paper-dialog {
width: 650px;
}
paper-spinner {
display: none;
}
paper-spinner[active] {
display: block;
}
paper-button paper-spinner {
width: 14px;
height: 14px;
margin-right: 20px;
}
</style>
`;
}
private _closeDialog(): void {
this._dialog.close();
}
private async _saveConfig(): Promise<void> {
if (!this.hass || !this._params) {
return;
}
this._saving = true;
delete this._params.config._frontendAuto;
try {
await saveConfig(this.hass, this._params.config, "json");
await migrateConfig(this.hass);
this._saving = false;
this._closeDialog();
this._params.reloadLovelace!();
} catch (err) {
alert(`Saving failed: ${err.message}`);
this._saving = false;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-dialog-save-config": HuiSaveConfig;
}
}
customElements.define(dialogTag, HuiSaveConfig);

View File

@ -1,4 +1,9 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import {
html,
LitElement,
PropertyDeclarations,
PropertyValues,
} from "@polymer/lit-element";
import { classMap } from "lit-html/directives/classMap";
import { TemplateResult } from "lit-html";
import yaml from "js-yaml";
@ -9,78 +14,65 @@ import "@polymer/paper-dialog/paper-dialog";
// tslint:disable-next-line
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-button/paper-button";
import "@polymer/paper-input/paper-textarea";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import { HomeAssistant } from "../../../types";
import { updateCardConfig } from "../common/data";
import {
addCard,
updateCardConfig,
LovelaceCardConfig,
} from "../../../data/lovelace";
import { fireEvent } from "../../../common/dom/fire_event";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import "./hui-yaml-editor";
import "./hui-card-picker";
import "./hui-card-preview";
// This is not a duplicate import, one is for types, one is for element.
// tslint:disable-next-line
import { HuiCardPreview } from "./hui-card-preview";
import { LovelaceCardEditor, LovelaceConfig } from "../types";
import { YamlChangedEvent, ConfigValue, ConfigError } from "./types";
import { LovelaceCardEditor } from "../types";
import {
YamlChangedEvent,
CardPickedEvent,
ConfigValue,
ConfigError,
} from "./types";
import { extYamlSchema } from "./yaml-ext-schema";
import { EntityConfig } from "../entity-rows/types";
import { getCardElementTag } from "../common/get-card-element-tag";
const CUSTOM_TYPE_PREFIX = "custom:";
declare global {
interface HASSDomEvents {
"yaml-changed": {
yaml: string;
};
"entities-changed": {
entities: EntityConfig[];
};
"config-changed": {
config: LovelaceCardConfig;
};
"cancel-edit-card": {};
}
}
export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
protected hass?: HomeAssistant;
private _cardId?: string;
private _originalConfig?: LovelaceConfig;
private _configElement?: LovelaceCardEditor | null;
private _uiEditor?: boolean;
private _configValue?: ConfigValue;
private _configState?: string;
private _loading?: boolean;
private _isToggleAvailable?: boolean;
private _saving: boolean;
static get properties(): PropertyDeclarations {
return {
_hass: {},
hass: {},
cardConfig: {},
viewId: {},
_cardId: {},
_originalConfig: {},
_configElement: {},
_configValue: {},
_configState: {},
_errorMsg: {},
_uiEditor: {},
_saving: {},
_loading: {},
_isToggleAvailable: {},
};
}
protected constructor() {
super();
this._saving = false;
}
set cardConfig(cardConfig: LovelaceConfig) {
this._originalConfig = cardConfig;
if (String(cardConfig.id) !== this._cardId) {
this._loading = true;
this._uiEditor = true;
this._configElement = undefined;
this._configValue = { format: "yaml", value: undefined };
this._configState = "OK";
this._isToggleAvailable = false;
this._cardId = String(cardConfig.id);
this._loadConfigElement();
}
}
public async showDialog(): Promise<void> {
// Wait till dialog is rendered.
if (this._dialog == null) {
await this.updateComplete;
}
this._dialog.open();
}
private get _dialog(): PaperDialogElement {
return this.shadowRoot!.querySelector("paper-dialog")!;
}
@ -89,11 +81,95 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
return this.shadowRoot!.querySelector("hui-card-preview")!;
}
public cardConfig?: LovelaceCardConfig;
public viewId?: string | number;
protected hass?: HomeAssistant;
private _cardId?: string;
private _configElement?: LovelaceCardEditor | null;
private _uiEditor?: boolean;
private _configValue?: ConfigValue;
private _configState?: string;
private _loading?: boolean;
private _saving: boolean;
private _errorMsg?: TemplateResult;
private _cardType?: string;
protected constructor() {
super();
this._saving = false;
}
public async showDialog(): Promise<void> {
// Wait till dialog is rendered.
if (this._dialog == null) {
await this.updateComplete;
}
this._dialog.open();
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (
!changedProperties.has("cardConfig") &&
!changedProperties.has("viewId")
) {
return;
}
this._configValue = { format: "yaml", value: undefined };
this._configState = "OK";
this._uiEditor = true;
this._errorMsg = undefined;
this._configElement = undefined;
if (this.cardConfig && String(this.cardConfig.id) !== this._cardId) {
this._loading = true;
this._cardId = String(this.cardConfig.id);
this._loadConfigElement(this.cardConfig);
} else {
this._cardId = undefined;
}
if (this.viewId && !this.cardConfig) {
this._resizeDialog();
}
}
protected render(): TemplateResult {
let content;
let preview;
if (this._configElement !== undefined) {
if (this._uiEditor) {
content = html`
<div class="element-editor">${this._configElement}</div>
`;
} else {
content = html`
<hui-yaml-editor
.hass="${this.hass}"
.cardId="${this._cardId}"
.yaml="${this._configValue!.value}"
@yaml-changed="${this._handleYamlChanged}"
></hui-yaml-editor>
`;
}
preview = html`
<hr />
<hui-card-preview .hass="${this.hass}"> </hui-card-preview>
`;
} else if (this.viewId && !this.cardConfig) {
content = html`
<hui-card-picker
.hass="${this.hass}"
@card-picked="${this._handleCardPicked}"
></hui-card-picker>
`;
}
return html`
${this.renderStyle()}
<paper-dialog with-backdrop>
<h2>${this.localize("ui.panel.lovelace.editor.edit.header")}</h2>
<h2>${this.localize("ui.panel.lovelace.editor.edit_card.header")}</h2>
<paper-spinner
?active="${this._loading}"
alt="Loading"
@ -103,31 +179,27 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
class="${classMap({ hidden: this._loading! })}"
>
${
this._uiEditor && this._configElement !== null
this._errorMsg
? html`
<div class="element-editor">${this._configElement}</div>
`
: html`
<hui-yaml-editor
.hass="${this.hass}"
.cardId="${this._cardId}"
.yaml="${this._configValue!.value}"
@yaml-changed="${this._handleYamlChanged}"
></hui-yaml-editor>
<div class="error">${this._errorMsg}</div>
`
: ""
}
<hui-card-preview .hass="${this.hass}"></hui-card-preview>
${content} ${preview}
</paper-dialog-scrollable>
${
!this._loading
? html`
<div class="paper-dialog-buttons">
<paper-button
?disabled="${!this._isToggleAvailable}"
?hidden="${!this._configValue || !this._configValue.value}"
?disabled="${
this._configElement === null || this._configState !== "OK"
}"
@click="${this._toggleEditor}"
>${
this.localize(
"ui.panel.lovelace.editor.edit.toggle_editor"
"ui.panel.lovelace.editor.edit_card.toggle_editor"
)
}</paper-button
>
@ -135,20 +207,19 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
>${this.localize("ui.common.cancel")}</paper-button
>
<paper-button
?disabled="${this._saving}"
?hidden="${!this._configValue || !this._configValue.value}"
?disabled="${this._saving || this._configState !== "OK"}"
@click="${this._save}"
>
<paper-spinner
?active="${this._saving}"
alt="Saving"
></paper-spinner>
${
this.localize("ui.panel.lovelace.editor.edit.save")
}</paper-button
${this.localize("ui.common.save")}</paper-button
>
</div>
`
: html``
: ""
}
</paper-dialog>
`;
@ -182,40 +253,25 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
display: none;
}
.element-editor {
margin-bottom: 16px;
margin-bottom: 8px;
}
.error {
color: #ef5350;
border-bottom: 1px solid #ef5350;
}
hr {
color: #000;
opacity: 0.12;
}
hui-card-preview {
padding-top: 8px;
margin-bottom: 4px;
display: block;
}
</style>
`;
}
private _toggleEditor(): void {
if (!this._isToggleAvailable) {
alert("You can't switch editor.");
return;
}
if (this._uiEditor && this._configValue!.format === "json") {
if (this._isConfigChanged()) {
this._configValue = {
format: "yaml",
value: yaml.safeDump(this._configValue!.value),
};
} else {
this._configValue = { format: "yaml", value: undefined };
}
this._uiEditor = !this._uiEditor;
} else if (this._configElement && this._configValue!.format === "yaml") {
this._configValue = {
format: "json",
value: yaml.safeLoad(this._configValue!.value, {
schema: extYamlSchema,
}),
};
this._configElement.setConfig(this._configValue!.value as LovelaceConfig);
this._uiEditor = !this._uiEditor;
}
this._resizeDialog();
}
private _save(): void {
this._saving = true;
this._updateConfigInBackend();
@ -237,6 +293,9 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
}
private _closeDialog(): void {
this.cardConfig = undefined;
this.viewId = undefined;
fireEvent(this, "cancel-edit-card");
this._dialog.close();
}
@ -254,34 +313,49 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
}
try {
await updateCardConfig(
this.hass!,
this._cardId!,
this._configValue!.value!,
this._configValue!.format
);
if (this.viewId) {
await addCard(
this.hass!,
String(this.viewId),
this._configValue!.value!,
this._configValue!.format
);
} else {
await updateCardConfig(
this.hass!,
this._cardId!,
this._configValue!.value!,
this._configValue!.format
);
}
fireEvent(this, "reload-lovelace");
this._closeDialog();
this._saveDone();
fireEvent(this, "reload-lovelace");
} catch (err) {
alert(`Saving failed: ${err.message}`);
this._saveDone();
}
}
private async _handleCardPicked(ev: CardPickedEvent): Promise<void> {
const succes = await this._loadConfigElement(ev.detail.config);
if (!succes) {
this._configValue = {
format: "yaml",
value: yaml.safeDump(ev.detail.config),
};
}
}
private _handleYamlChanged(ev: YamlChangedEvent): void {
this._configValue = { format: "yaml", value: ev.detail.yaml };
try {
const config = yaml.safeLoad(this._configValue.value, {
schema: extYamlSchema,
}) as LovelaceConfig;
}) as LovelaceCardConfig;
this._updatePreview(config);
this._configState = "OK";
if (!this._isToggleAvailable && this._configElement !== null) {
this._isToggleAvailable = true;
}
} catch (err) {
this._isToggleAvailable = false;
this._configState = "YAML_ERROR";
this._setPreviewError({
type: "YAML Error",
@ -290,12 +364,12 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
}
}
private _handleUIConfigChanged(value: LovelaceConfig): void {
private _handleUIConfigChanged(value: LovelaceCardConfig): void {
this._configValue = { format: "json", value };
this._updatePreview(value);
}
private _updatePreview(config: LovelaceConfig) {
private _updatePreview(config: LovelaceCardConfig) {
if (!this._previewEl) {
return;
}
@ -318,8 +392,42 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
this._resizeDialog();
}
private async _toggleEditor(): Promise<void> {
if (this._uiEditor && this._configValue!.format === "json") {
if (this._isConfigChanged()) {
this._configValue = {
format: "yaml",
value: yaml.safeDump(this._configValue!.value),
};
} else {
this._configValue = { format: "yaml", value: undefined };
}
this._uiEditor = !this._uiEditor;
} else if (this._configElement && this._configValue!.format === "yaml") {
const yamlConfig = this._configValue!.value;
const cardConfig = yaml.safeLoad(yamlConfig, {
schema: extYamlSchema,
}) as LovelaceCardConfig;
this._uiEditor = !this._uiEditor;
if (cardConfig.type !== this._cardType) {
const succes = await this._loadConfigElement(cardConfig);
if (!succes) {
this._loadedDialog();
}
this._cardType = cardConfig.type;
} else {
this._configValue = {
format: "json",
value: cardConfig,
};
this._configElement.setConfig(cardConfig);
}
}
this._resizeDialog();
}
private _isConfigValid() {
if (!this._cardId || !this._configValue || !this._configValue.value) {
if (!this._configValue || !this._configValue.value) {
return false;
}
if (this._configState === "OK") {
@ -330,43 +438,59 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
}
private _isConfigChanged(): boolean {
if (this.viewId) {
return true;
}
const configValue =
this._configValue!.format === "yaml"
? yaml.safeDump(this._configValue!.value)
: this._configValue!.value;
return JSON.stringify(configValue) !== JSON.stringify(this._originalConfig);
return JSON.stringify(configValue) !== JSON.stringify(this.cardConfig);
}
private async _loadConfigElement(): Promise<void> {
if (!this._originalConfig) {
return;
private async _loadConfigElement(conf: LovelaceCardConfig): Promise<boolean> {
if (!conf) {
return false;
}
const conf = this._originalConfig;
const tag = conf.type.startsWith(CUSTOM_TYPE_PREFIX)
? conf!.type.substr(CUSTOM_TYPE_PREFIX.length)
: `hui-${conf!.type}-card`;
this._errorMsg = undefined;
this._loading = true;
this._configElement = undefined;
const tag = getCardElementTag(conf.type);
const elClass = customElements.get(tag);
let configElement;
try {
if (elClass && elClass.getConfigElement) {
configElement = await elClass.getConfigElement();
} catch (err) {
this._configElement = null;
} else {
this._uiEditor = false;
return;
this._configElement = null;
return false;
}
this._isToggleAvailable = true;
try {
configElement.setConfig(conf);
} catch (err) {
this._errorMsg = html`
Your config is not supported by the UI editor:<br /><b>${err.message}</b
><br />Falling back to YAML editor.
`;
this._uiEditor = false;
this._configElement = null;
return false;
}
configElement.setConfig(conf);
configElement.hass = this.hass;
configElement.addEventListener("config-changed", (ev) =>
this._handleUIConfigChanged(ev.detail.config)
);
this._configValue = { format: "json", value: conf };
this._configElement = configElement;
await this.updateComplete;
this._updatePreview(conf);
return true;
}
}

View File

@ -0,0 +1,277 @@
import {
html,
LitElement,
PropertyDeclarations,
PropertyValues,
} from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-spinner/paper-spinner";
import "@polymer/paper-tabs/paper-tab";
import "@polymer/paper-tabs/paper-tabs";
import "@polymer/paper-dialog/paper-dialog";
// This is not a duplicate import, one is for types, one is for element.
// tslint:disable-next-line
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-button/paper-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "../components/hui-entity-editor";
import "./config-elements/hui-view-editor";
import { HomeAssistant } from "../../../types";
import {
addView,
updateViewConfig,
LovelaceViewConfig,
} from "../../../data/lovelace";
import { fireEvent } from "../../../common/dom/fire_event";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { EntitiesEditorEvent, ViewEditEvent } from "./types";
import { processEditorEntities } from "./process-editor-entities";
import { EntityConfig } from "../entity-rows/types";
export class HuiEditView extends hassLocalizeLitMixin(LitElement) {
static get properties(): PropertyDeclarations {
return {
hass: {},
viewConfig: {},
add: {},
_config: {},
_badges: {},
_saving: {},
_curTab: {},
};
}
public viewConfig?: LovelaceViewConfig;
public add?: boolean;
public reloadLovelace?: () => {};
protected hass?: HomeAssistant;
private _config?: LovelaceViewConfig;
private _badges?: EntityConfig[];
private _saving: boolean;
private _curTabIndex: number;
private _curTab?: string;
protected constructor() {
super();
this._saving = false;
this._curTabIndex = 0;
}
public async showDialog(): Promise<void> {
// Wait till dialog is rendered.
if (this._dialog == null) {
await this.updateComplete;
}
this._dialog.open();
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (!changedProperties.has("viewConfig") && !changedProperties.has("add")) {
return;
}
if (
this.viewConfig &&
(!changedProperties.get("viewConfig") ||
this.viewConfig.id !==
(changedProperties.get("viewConfig") as LovelaceViewConfig).id)
) {
const { cards, badges, ...viewConfig } = this.viewConfig;
this._config = viewConfig;
this._badges = processEditorEntities(badges);
} else if (changedProperties.has("add")) {
this._config = {};
this._badges = [];
}
this._resizeDialog();
}
private get _dialog(): PaperDialogElement {
return this.shadowRoot!.querySelector("paper-dialog")!;
}
protected render(): TemplateResult {
let content;
switch (this._curTab) {
case "tab-settings":
content = html`
<hui-view-editor
.hass="${this.hass}"
.config="${this._config}"
@view-config-changed="${this._viewConfigChanged}"
></hui-view-editor>
`;
break;
case "tab-badges":
content = html`
<hui-entity-editor
.hass="${this.hass}"
.entities="${this._badges}"
@entities-changed="${this._badgesChanged}"
></hui-entity-editor>
`;
break;
case "tab-cards":
content = html`
Cards
`;
break;
}
return html`
${this.renderStyle()}
<paper-dialog with-backdrop>
<h2>${this.localize("ui.panel.lovelace.editor.edit_view.header")}</h2>
<paper-tabs
scrollable
hide-scroll-buttons
.selected="${this._curTabIndex}"
@selected-item-changed="${this._handleTabSelected}"
>
<paper-tab id="tab-settings">Settings</paper-tab>
<paper-tab id="tab-badges">Badges</paper-tab>
</paper-tabs>
<paper-dialog-scrollable> ${content} </paper-dialog-scrollable>
<div class="paper-dialog-buttons">
<paper-button @click="${this._closeDialog}"
>${this.localize("ui.common.cancel")}</paper-button
>
<paper-button
?disabled="${!this._config || this._saving}"
@click="${this._save}"
>
<paper-spinner
?active="${this._saving}"
alt="Saving"
></paper-spinner>
${this.localize("ui.common.save")}</paper-button
>
</div>
</paper-dialog>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
paper-dialog {
width: 650px;
}
paper-tabs {
--paper-tabs-selection-bar-color: var(--primary-color);
text-transform: uppercase;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
paper-button paper-spinner {
width: 14px;
height: 14px;
margin-right: 20px;
}
paper-spinner {
display: none;
}
paper-spinner[active] {
display: block;
}
.hidden {
display: none;
}
.error {
color: #ef5350;
border-bottom: 1px solid #ef5350;
}
</style>
`;
}
private _save(): void {
this._saving = true;
this._updateConfigInBackend();
}
private async _resizeDialog(): Promise<void> {
await this.updateComplete;
fireEvent(this._dialog, "iron-resize");
}
private _closeDialog(): void {
this._curTabIndex = 0;
this._config = {};
this._badges = [];
this.viewConfig = undefined;
this._dialog.close();
}
private _handleTabSelected(ev: CustomEvent): void {
if (!ev.detail.value) {
return;
}
this._curTab = ev.detail.value.id;
this._resizeDialog();
}
private async _updateConfigInBackend(): Promise<void> {
if (!this._config) {
return;
}
if (!this._isConfigChanged()) {
this._closeDialog();
this._saving = false;
return;
}
if (this._badges) {
this._config.badges = this._badges.map((entityConf) => {
return entityConf.entity;
});
}
try {
if (this.add) {
this._config.cards = [];
await addView(this.hass!, this._config, "json");
} else {
await updateViewConfig(
this.hass!,
this.viewConfig!.id!,
this._config,
"json"
);
}
this.reloadLovelace!();
this._closeDialog();
this._saving = false;
} catch (err) {
alert(`Saving failed: ${err.message}`);
this._saving = false;
}
}
private _viewConfigChanged(ev: ViewEditEvent): void {
if (ev.detail && ev.detail.config) {
this._config = ev.detail.config;
}
}
private _badgesChanged(ev: EntitiesEditorEvent): void {
if (!this._badges || !this.hass || !ev.detail || !ev.detail.entities) {
return;
}
this._badges = ev.detail.entities;
}
private _isConfigChanged(): boolean {
if (!this.add) {
return true;
}
return JSON.stringify(this._config) !== JSON.stringify(this.viewConfig);
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-edit-view": HuiEditView;
}
}
customElements.define("hui-edit-view", HuiEditView);

View File

@ -11,7 +11,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { HomeAssistant } from "../../../types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { migrateConfig } from "../common/data";
import { migrateConfig } from "../../../data/lovelace";
export class HuiMigrateConfig extends hassLocalizeLitMixin(LitElement) {
protected hass?: HomeAssistant;
@ -95,6 +95,7 @@ export class HuiMigrateConfig extends hassLocalizeLitMixin(LitElement) {
try {
await migrateConfig(this.hass!);
this._closeDialog();
this._migrating = false;
fireEvent(this, "reload-lovelace");
} catch (err) {
alert(`Migration failed: ${err.message}`);

View File

@ -5,7 +5,7 @@ import "@polymer/paper-spinner/paper-spinner";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { getCardConfig } from "../common/data";
import { getCardConfig } from "../../../data/lovelace";
export class HuiYAMLEditor extends LitElement {
public cardId?: string;

View File

@ -1,4 +1,4 @@
import { LovelaceConfig } from "../types";
import { LovelaceCardConfig, LovelaceViewConfig } from "../../../data/lovelace";
import { EntityConfig } from "../entity-rows/types";
export interface YamlChangedEvent extends Event {
@ -7,9 +7,21 @@ export interface YamlChangedEvent extends Event {
};
}
export interface CardPickedEvent extends Event {
detail: {
config: LovelaceCardConfig;
};
}
export interface ViewEditEvent extends Event {
detail: {
config: LovelaceViewConfig;
};
}
export interface ConfigValue {
format: "json" | "yaml";
value?: string | LovelaceConfig;
value?: string | LovelaceCardConfig;
}
export interface ConfigError {
@ -30,3 +42,7 @@ export interface EditorTarget extends EventTarget {
checked?: boolean;
configValue?: string;
}
export interface CardPickTarget extends EventTarget {
type: string;
}

View File

@ -41,13 +41,21 @@ export class HuiIconElement extends hassLocalizeLitMixin(LitElement)
<ha-icon
.icon="${this._config.icon}"
.title="${computeTooltip(this.hass!, this._config)}"
@ha-click="${() => handleClick(this, this.hass!, this._config!, false)}"
@ha-hold="${() => handleClick(this, this.hass!, this._config!, true)}"
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
></ha-icon>
`;
}
private _handleTap() {
handleClick(this, this.hass!, this._config!, false);
}
private _handleHold() {
handleClick(this, this.hass!, this._config!, true);
}
private renderStyle(): TemplateResult {
return html`
<style>

View File

@ -33,7 +33,10 @@ export class HuiImageElement extends hassLocalizeLitMixin(LitElement)
throw Error("Error in element configuration");
}
this.classList.toggle("clickable", config.tap_action !== "none");
this.classList.toggle(
"clickable",
config.tap_action && config.tap_action.action !== "none"
);
this._config = config;
}
@ -54,7 +57,7 @@ export class HuiImageElement extends hassLocalizeLitMixin(LitElement)
.stateFilter="${this._config.state_filter}"
.title="${computeTooltip(this.hass!, this._config)}"
.aspectRatio="${this._config.aspect_ratio}"
@ha-click="${this._handleClick}"
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
></hui-image>
@ -76,7 +79,7 @@ export class HuiImageElement extends hassLocalizeLitMixin(LitElement)
`;
}
private _handleClick() {
private _handleTap() {
handleClick(this, this.hass!, this._config!, false);
}

View File

@ -43,8 +43,8 @@ class HuiStateLabelElement extends hassLocalizeLitMixin(LitElement)
${this.renderStyle()}
<div
.title="${computeTooltip(this.hass!, this._config)}"
@ha-click="${() => handleClick(this, this.hass!, this._config!, false)}"
@ha-hold="${() => handleClick(this, this.hass!, this._config!, true)}"
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
>
${this._config.prefix}${
@ -56,6 +56,14 @@ class HuiStateLabelElement extends hassLocalizeLitMixin(LitElement)
`;
}
private _handleTap() {
handleClick(this, this.hass!, this._config!, false);
}
private _handleHold() {
handleClick(this, this.hass!, this._config!, true);
}
private renderStyle(): TemplateResult {
return html`
<style>

View File

@ -1,14 +1,15 @@
import { HomeAssistant } from "../../../types";
import { ActionConfig } from "../../../data/lovelace";
export interface LovelaceElementConfig {
type: string;
style: object;
entity?: string;
hold_action?: string;
navigation_path?: string;
hold_action?: ActionConfig;
service?: string;
service_data?: object;
tap_action?: string;
navigation_path?: string;
tap_action?: ActionConfig;
title?: string;
}

View File

@ -3,16 +3,19 @@ import { TemplateResult } from "lit-html";
class HuiErrorEntityRow extends LitElement {
public entity?: string;
public error?: string;
static get properties() {
return {
error: {},
entity: {},
};
}
protected render(): TemplateResult {
return html`
${this.renderStyle()} Entity not available: ${this.entity || ""}
${this.renderStyle()} ${this.error || "Entity not available"}:
${this.entity || ""}
`;
}

View File

@ -1,165 +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 "../components/hui-generic-entity-row";
import LocalizeMixin from "../../../mixins/localize-mixin";
const SUPPORT_PAUSE = 1;
const SUPPORT_NEXT_TRACK = 32;
const SUPPORTS_PLAY = 16384;
const OFF_STATES = ["off", "idle"];
/*
* @appliesMixin LocalizeMixin
*/
class HuiMediaPlayerEntityRow extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
${this.styleTemplate}
<hui-generic-entity-row
hass="[[hass]]"
config="[[_config]]"
show-secondary="false"
>
${this.mediaPlayerControlTemplate}
</hui-generic-entity-row>
`;
}
static get styleTemplate() {
return html`
<style>
.controls {
white-space: nowrap;
}
</style>
`;
}
static get mediaPlayerControlTemplate() {
return html`
<template is="dom-if" if="[[!_isOff(_stateObj.state)]]">
<div class="controls">
<template is="dom-if" if="[[_computeControlIcon(_stateObj)]]">
<paper-icon-button
icon="[[_computeControlIcon(_stateObj)]]"
on-click="_playPause"
></paper-icon-button>
</template>
<template is="dom-if" if="[[_supportsNext(_stateObj)]]">
<paper-icon-button
icon="hass:skip-next"
on-click="_nextTrack"
></paper-icon-button>
</template>
</div>
</template>
<template is="dom-if" if="[[_isOff(_stateObj.state)]]">
<div>[[_computeState(_stateObj.state)]]</div>
</template>
<div slot="secondary">[[_computeMediaTitle(_stateObj)]]</div>
`;
}
static get properties() {
return {
hass: Object,
_config: Object,
_stateObj: {
type: Object,
computed: "_computeStateObj(hass.states, _config.entity)",
},
};
}
_computeStateObj(states, entityId) {
return states && entityId in states ? states[entityId] : null;
}
setConfig(config) {
if (!config || !config.entity) {
throw new Error("Entity not configured.");
}
this._config = config;
}
_computeControlIcon(stateObj) {
if (!stateObj) return null;
if (stateObj.state !== "playing") {
return stateObj.attributes.supported_features & SUPPORTS_PLAY
? "hass:play"
: "";
}
return stateObj.attributes.supported_features & SUPPORT_PAUSE
? "hass:pause"
: "hass:stop";
}
_computeMediaTitle(stateObj) {
if (!stateObj || this._isOff(stateObj.state)) return null;
let prefix;
let suffix;
switch (stateObj.attributes.media_content_type) {
case "music":
prefix = stateObj.attributes.media_artist;
suffix = stateObj.attributes.media_title;
break;
case "tvshow":
prefix = stateObj.attributes.media_series_title;
suffix = stateObj.attributes.media_title;
break;
default:
prefix =
stateObj.attributes.media_title ||
stateObj.attributes.app_name ||
stateObj.state;
suffix = "";
}
return prefix && suffix ? `${prefix}: ${suffix}` : prefix || suffix || "";
}
_computeState(state) {
return (
this.localize(`state.media_player.${state}`) ||
this.localize(`state.default.${state}`) ||
state
);
}
_callService(service) {
this.hass.callService("media_player", service, {
entity_id: this._config.entity,
});
}
_playPause(event) {
event.stopPropagation();
this._callService("media_play_pause");
}
_nextTrack(event) {
event.stopPropagation();
if (this._stateObj.attributes.supported_features & SUPPORT_NEXT_TRACK) {
this._callService("media_next_track");
}
}
_isOff(state) {
return OFF_STATES.includes(state);
}
_supportsNext(stateObj) {
return (
stateObj && stateObj.attributes.supported_features & SUPPORT_NEXT_TRACK
);
}
}
customElements.define("hui-media-player-entity-row", HuiMediaPlayerEntityRow);

View File

@ -0,0 +1,169 @@
import { html, LitElement } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-icon-button/paper-icon-button";
import "../components/hui-generic-entity-row";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { EntityRow, EntityConfig } from "./types";
import { HomeAssistant } from "../../../types";
import { HassEntity } from "home-assistant-js-websocket";
import { supportsFeature } from "../../../common/entity/supports-feature";
import {
SUPPORTS_PLAY,
SUPPORT_NEXT_TRACK,
OFF_STATES,
SUPPORT_PAUSE,
} from "../../../data/media-player";
class HuiMediaPlayerEntityRow extends hassLocalizeLitMixin(LitElement)
implements EntityRow {
public hass?: HomeAssistant;
private _config?: EntityConfig;
static get properties() {
return {
hass: {},
_config: {},
};
}
public setConfig(config: EntityConfig): void {
if (!config || !config.entity) {
throw new Error("Invalid Configuration: 'entity' required");
}
this._config = config;
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
const stateObj = this.hass.states[this._config.entity];
if (!stateObj) {
return html`
<hui-error-entity-row
.entity="${this._config.entity}"
></hui-error-entity-row>
`;
}
return html`
${this.renderStyle()}
<hui-generic-entity-row
.hass="${this.hass}"
.config="${this._config}"
.showSecondary="false"
>
${
OFF_STATES.includes(stateObj.state)
? html`
<div>
${
this.localize(`state.media_player.${stateObj.state}`) ||
this.localize(`state.default.${stateObj.state}`) ||
stateObj.state
}
</div>
`
: html`
<div class="controls">
${
stateObj.state !== "playing" &&
!supportsFeature(stateObj, SUPPORTS_PLAY)
? ""
: html`
<paper-icon-button
icon="${this._computeControlIcon(stateObj)}"
@click="${this._playPause}"
></paper-icon-button>
`
}
${
supportsFeature(stateObj, SUPPORT_NEXT_TRACK)
? html`
<paper-icon-button
icon="hass:skip-next"
@click="${this._nextTrack}"
></paper-icon-button>
`
: ""
}
</div>
<div slot="secondary">${this._computeMediaTitle(stateObj)}</div>
`
}
</hui-generic-entity-row>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
.controls {
white-space: nowrap;
}
</style>
`;
}
private _computeControlIcon(stateObj: HassEntity): string {
if (stateObj.state !== "playing") {
return "hass:play";
}
// tslint:disable-next-line:no-bitwise
return supportsFeature(stateObj, SUPPORT_PAUSE)
? "hass:pause"
: "hass:stop";
}
private _computeMediaTitle(stateObj: HassEntity): string {
let prefix;
let suffix;
switch (stateObj.attributes.media_content_type) {
case "music":
prefix = stateObj.attributes.media_artist;
suffix = stateObj.attributes.media_title;
break;
case "tvshow":
prefix = stateObj.attributes.media_series_title;
suffix = stateObj.attributes.media_title;
break;
default:
prefix =
stateObj.attributes.media_title ||
stateObj.attributes.app_name ||
stateObj.state;
suffix = "";
}
return prefix && suffix ? `${prefix}: ${suffix}` : prefix || suffix || "";
}
private _playPause(ev: MouseEvent): void {
ev.stopPropagation();
this.hass!.callService("media_player", "media_play_pause", {
entity_id: this._config!.entity,
});
}
private _nextTrack(ev: MouseEvent): void {
ev.stopPropagation();
this.hass!.callService("media_player", "media_next_track", {
entity_id: this._config!.entity,
});
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-media-player-entity-row": HuiMediaPlayerEntityRow;
}
}
customElements.define("hui-media-player-entity-row", HuiMediaPlayerEntityRow);

View File

@ -0,0 +1,85 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "../components/hui-generic-entity-row";
import "../components/hui-timestamp-display";
import "./hui-error-entity-row";
import { HomeAssistant } from "../../../types";
import { EntityRow, EntityConfig } from "./types";
interface SensorEntityConfig extends EntityConfig {
format?: "relative" | "date" | "time" | "datetime";
}
class HuiSensorEntityRow extends LitElement implements EntityRow {
public hass?: HomeAssistant;
private _config?: SensorEntityConfig;
static get properties(): PropertyDeclarations {
return {
hass: {},
_config: {},
};
}
public setConfig(config: SensorEntityConfig): void {
if (!config) {
throw new Error("Configuration error");
}
this._config = config;
}
protected render(): TemplateResult {
if (!this._config || !this.hass) {
return html``;
}
const stateObj = this.hass.states[this._config.entity];
if (!stateObj) {
return html`
<hui-error-entity-row
.entity="${this._config.entity}"
></hui-error-entity-row>
`;
}
return html`
${this.renderStyle()}
<hui-generic-entity-row .hass="${this.hass}" .config="${this._config}">
<div>
${
stateObj.attributes.device_class === "timestamp"
? html`
<hui-timestamp-display
.hass="${this.hass}"
.ts="${new Date(stateObj.state)}"
.format="${this._config.format}"
></hui-timestamp-display>
`
: stateObj.state
}
</div>
</hui-generic-entity-row>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
div {
text-align: right;
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-sensor-entity-row": HuiSensorEntityRow;
}
}
customElements.define("hui-sensor-entity-row", HuiSensorEntityRow);

View File

@ -2,11 +2,16 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/paper-button/paper-button";
import { registerSaveDialog } from "./editor/hui-dialog-save-config";
import { fetchConfig } from "../../data/lovelace";
import "../../layouts/hass-loading-screen";
import "../../layouts/hass-error-screen";
import "./hui-root";
import localizeMixin from "../../mixins/localize-mixin";
class Lovelace extends PolymerElement {
let registeredDialog = false;
class Lovelace extends localizeMixin(PolymerElement) {
static get template() {
return html`
<style>
@ -110,16 +115,30 @@ class Lovelace extends PolymerElement {
async _fetchConfig() {
try {
const conf = await this.hass.callWS({ type: "lovelace/config" });
const conf = await fetchConfig(this.hass);
this.setProperties({
_config: conf,
_state: "loaded",
});
} catch (err) {
this.setProperties({
_state: "error",
_errorMsg: err.message,
});
if (err.code === "file_not_found") {
const {
generateLovelaceConfig,
} = await import("./common/generate-lovelace-config");
this.setProperties({
_config: generateLovelaceConfig(this.hass, this.localize),
_state: "loaded",
});
if (!registeredDialog) {
registeredDialog = true;
registerSaveDialog(this);
}
} else {
this.setProperties({
_state: "error",
_errorMsg: err.message,
});
}
}
}

View File

@ -4,6 +4,7 @@ import "@polymer/app-layout/app-scroll-effects/effects/waterfall";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/app-route/app-route";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-button/paper-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-menu-button/paper-menu-button";
@ -16,6 +17,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import scrollToTarget from "../../common/dom/scroll-to-target";
import EventsMixin from "../../mixins/events-mixin";
import localizeMixin from "../../mixins/localize-mixin";
import NavigateMixin from "../../mixins/navigate-mixin";
import "../../layouts/ha-app-layout";
@ -29,14 +31,18 @@ import "./components/notifications/hui-notifications-button";
import "./hui-unused-entities";
import "./hui-view";
import debounce from "../../common/util/debounce";
import createCardElement from "./common/create-card-element";
import { showSaveDialog } from "./editor/hui-dialog-save-config";
import { showEditViewDialog } from "./editor/hui-dialog-edit-view";
import { confDeleteView } from "./editor/delete-view";
// CSS and JS should only be imported once. Modules and HTML are safe.
const CSS_CACHE = {};
const JS_CACHE = {};
class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
class HUIRoot extends NavigateMixin(
EventsMixin(localizeMixin(PolymerElement))
) {
static get template() {
return html`
<style include='ha-style'>
@ -54,9 +60,24 @@ class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
--paper-tabs-selection-bar-color: var(--text-primary-color, #FFF);
text-transform: uppercase;
}
#add-view {
background: var(--paper-fab-background, var(--accent-color));
position: absolute;
height: 44px;
}
app-toolbar a {
color: var(--text-primary-color, white);
}
paper-button.warning:not([disabled]) {
color: var(--google-red-500);
}
app-toolbar.secondary {
background-color: var(--light-primary-color);
color: var(--primary-text-color, #333);
font-size: 14px;
font-weight: 500;
height: auto;
}
#view {
min-height: calc(100vh - 112px);
/**
@ -103,7 +124,7 @@ class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
<paper-listbox on-iron-select="_deselect" slot="dropdown-content">
<paper-item on-click="_handleRefresh">Refresh</paper-item>
<paper-item on-click="_handleUnusedEntities">Unused entities</paper-item>
<paper-item on-click="_editModeEnable">Configure UI (alpha)</paper-item>
<paper-item on-click="_editModeEnable">[[localize("ui.panel.lovelace.editor.configure_ui")]] (alpha)</paper-item>
<paper-item on-click="_handleHelp">Help</paper-item>
</paper-listbox>
</paper-menu-button>
@ -115,7 +136,7 @@ class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
icon='hass:close'
on-click='_editModeDisable'
></paper-icon-button>
<div main-title>Edit UI</div>
<div main-title>[[localize("ui.panel.lovelace.editor.header")]]</div>
</app-toolbar>
</template>
@ -131,10 +152,20 @@ class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
</template>
</paper-tab>
</template>
<template is='dom-if' if="[[_editMode]]">
<paper-button id="add-view" on-click="_addView">
<ha-icon title=[[localize("ui.panel.lovelace.editor.edit_view.add")]] icon="hass:plus"></ha-icon>
</paper-button>
</template>
</paper-tabs>
</div>
</app-header>
<template is='dom-if' if="[[_editMode]]">
<app-toolbar class="secondary">
<paper-button on-click="_editView">[[localize("ui.panel.lovelace.editor.edit_view.edit")]]</paper-button>
<paper-button class="warning" on-click="_deleteView">[[localize("ui.panel.lovelace.editor.edit_view.delete")]]</paper-button>
</app-toolbar>
</template>
<div id='view' on-rebuild-view='_debouncedConfigChanged'></div>
</app-header-layout>
`;
@ -274,6 +305,16 @@ class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
}
_editModeEnable() {
if (this.config._frontendAuto) {
showSaveDialog(this, {
config: this.config,
reloadLovelace: () => {
this.fire("config-refresh");
this._editMode = true;
},
});
return;
}
this._editMode = true;
}
@ -285,10 +326,51 @@ class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
this._selectView(this._curView);
}
_editView() {
showEditViewDialog(this, {
viewConfig: this.config.views[this._curView],
add: false,
reloadLovelace: () => {
this.fire("config-refresh");
},
});
}
_addView() {
showEditViewDialog(this, {
add: true,
reloadLovelace: () => {
this.fire("config-refresh");
},
});
}
_deleteView() {
const viewConfig = this.config.views[this._curView];
if (viewConfig.cards && viewConfig.cards.length > 0) {
alert(
"You can't delete a view that has card in them. Remove the cards first."
);
return;
}
if (!viewConfig.id) {
this._editView();
return;
}
confDeleteView(this.hass, viewConfig.id, () => {
this.fire("config-refresh");
this._navigateView(0);
});
}
_handleViewSelected(ev) {
const index = ev.detail.selected;
if (index !== this._curView) {
const id = this.config.views[index].id || index;
this._navigateView(index);
}
_navigateView(viewIndex) {
if (viewIndex !== this._curView) {
const id = this.config.views[viewIndex].id || viewIndex;
this.navigate(`/lovelace/${id}`);
}
scrollToTarget(this, this.$.layout.header.scrollTarget);
@ -308,7 +390,7 @@ class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
if (viewIndex === "unused") {
view = document.createElement("hui-unused-entities");
view.config = this.config;
view.setConfig(this.config);
} else {
const viewConfig = this.config.views[this._curView];
if (viewConfig.panel) {

View File

@ -1,61 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import computeUnusedEntities from "./common/compute-unused-entities";
import createCardElement from "./common/create-card-element";
import "./cards/hui-entities-card.ts";
class HuiUnusedEntities extends PolymerElement {
static get template() {
return html`
<style>
#root {
max-width: 600px;
margin: 0 auto;
padding: 8px 0;
}
</style>
<div id="root"></div>
`;
}
static get properties() {
return {
hass: {
type: Object,
observer: "_hassChanged",
},
config: {
type: Object,
observer: "_configChanged",
},
};
}
_configChanged(config) {
const root = this.$.root;
if (root.lastChild) root.removeChild(root.lastChild);
const entities = computeUnusedEntities(this.hass, config).map((entity) => ({
entity,
secondary_info: "entity-id",
}));
const cardConfig = {
type: "entities",
title: "Unused entities",
entities,
show_header_toggle: false,
};
const element = createCardElement(cardConfig);
element.hass = this.hass;
root.appendChild(element);
}
_hassChanged(hass) {
const root = this.$.root;
if (!root.lastChild) return;
root.lastChild.hass = hass;
}
}
customElements.define("hui-unused-entities", HuiUnusedEntities);

View File

@ -0,0 +1,87 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import "./cards/hui-entities-card";
import computeUnusedEntities from "./common/compute-unused-entities";
import createCardElement from "./common/create-card-element";
import { HomeAssistant } from "../../types";
import { TemplateResult } from "lit-html";
import { LovelaceCard } from "./types";
export class HuiUnusedEntities extends LitElement {
private _hass?: HomeAssistant;
private _config?: object;
private _element?: LovelaceCard;
static get properties(): PropertyDeclarations {
return {
_hass: {},
_config: {},
};
}
set hass(hass: HomeAssistant) {
this._hass = hass;
if (!this._element) {
this._createElement();
return;
}
this._element.hass = this._hass;
}
public setConfig(config: object): void {
if (!config) {
throw new Error("Card config incorrect");
}
this._config = config;
this._createElement();
}
protected render(): TemplateResult {
if (!this._config || !this._hass) {
return html``;
}
return html`
${this.renderStyle()}
<div id="root">${this._element}</div>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
#root {
max-width: 600px;
margin: 0 auto;
padding: 8px 0;
}
</style>
`;
}
private _createElement(): void {
if (this._hass) {
const entities = computeUnusedEntities(this._hass, this._config).map(
(entity) => ({
entity,
secondary_info: "entity-id",
})
);
this._element = createCardElement({
type: "entities",
title: "Unused entities",
entities,
show_header_toggle: false,
});
this._element!.hass = this._hass;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-unused-entities": HuiUnusedEntities;
}
}
customElements.define("hui-unused-entities", HuiUnusedEntities);

View File

@ -1,15 +1,19 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/paper-fab/paper-fab";
import "../../components/entity/ha-state-label-badge";
import "./components/hui-card-options.ts";
import "./components/hui-card-options";
import applyThemesOnElement from "../../common/dom/apply_themes_on_element";
import EventsMixin from "../../mixins/events-mixin";
import localizeMixin from "../../mixins/localize-mixin";
import createCardElement from "./common/create-card-element";
import { computeCardSize } from "./common/compute-card-size";
import { showEditCardDialog } from "./editor/hui-dialog-edit-card";
class HUIView extends PolymerElement {
class HUIView extends localizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style>
@ -18,6 +22,7 @@ class HUIView extends PolymerElement {
padding: 4px 4px 0;
transform: translateZ(0);
position: relative;
min-height: calc(100vh - 155px);
}
#badges {
@ -44,6 +49,18 @@ class HUIView extends PolymerElement {
margin: 4px 4px 8px;
}
paper-fab {
position: sticky;
float: right;
bottom: 16px;
right: 16px;
z-index: 1;
}
paper-fab[hidden] {
display: none;
}
@media (max-width: 500px) {
:host {
padding-left: 0;
@ -64,6 +81,13 @@ class HUIView extends PolymerElement {
</style>
<div id="badges"></div>
<div id="columns"></div>
<paper-fab
hidden$="{{!editMode}}"
elevated="2"
icon="hass:plus"
title=[[localize("ui.panel.lovelace.editor.edit_card.add")]]
on-click="_addCard"
></paper-fab>
`;
}
@ -93,6 +117,16 @@ class HUIView extends PolymerElement {
this._badges = [];
}
_addCard() {
showEditCardDialog(this, {
viewId: this.config.id,
add: true,
reloadLovelace: () => {
this.fire("config-refresh");
},
});
}
_createBadges(config) {
const root = this.$.badges;
while (root.lastChild) {

View File

@ -1,17 +1,13 @@
import { HomeAssistant } from "../../types";
export interface LovelaceConfig {
type: string;
id: string;
}
import { LovelaceCardConfig } from "../../data/lovelace";
export interface LovelaceCard extends HTMLElement {
hass?: HomeAssistant;
getCardSize(): number;
setConfig(config: LovelaceConfig): void;
setConfig(config: LovelaceCardConfig): void;
}
export interface LovelaceCardEditor extends HTMLElement {
hass?: HomeAssistant;
setConfig(config: LovelaceConfig): void;
setConfig(config: LovelaceCardConfig): void;
}

View File

@ -1,3 +1,5 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-scroll-effects/effects/waterfall";
import "@polymer/app-layout/app-toolbar/app-toolbar";
@ -6,23 +8,21 @@ import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/iron-pages/iron-pages";
import "@polymer/paper-tabs/paper-tab";
import "@polymer/paper-tabs/paper-tabs";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/ha-cards";
import "../components/ha-icon";
import "../components/ha-menu-button";
import "../components/ha-start-voice-button";
import "../../components/ha-cards";
import "../../components/ha-icon";
import "../../components/ha-menu-button";
import "../../components/ha-start-voice-button";
import "./ha-app-layout";
import "../../layouts/ha-app-layout";
import extractViews from "../common/entity/extract_views";
import getViewEntities from "../common/entity/get_view_entities";
import computeStateName from "../common/entity/compute_state_name";
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 extractViews from "../../common/entity/extract_views";
import getViewEntities from "../../common/entity/get_view_entities";
import computeStateName from "../../common/entity/compute_state_name";
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";
const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
const ALWAYS_SHOW_DOMAIN = ["persistent_notification", "configurator"];
@ -46,6 +46,10 @@ class PartialCards extends EventsMixin(NavigateMixin(PolymerElement)) {
background-color: var(--secondary-background-color, #e5e5e5);
}
iron-pages {
height: 100%;
}
paper-tabs {
margin-left: 12px;
--paper-tabs-selection-bar-color: var(--text-primary-color, #fff);
@ -205,7 +209,7 @@ class PartialCards extends EventsMixin(NavigateMixin(PolymerElement)) {
showTabs: {
type: Boolean,
value: false,
value: true,
},
};
}
@ -416,4 +420,4 @@ class PartialCards extends EventsMixin(NavigateMixin(PolymerElement)) {
}
}
customElements.define("partial-cards", PartialCards);
customElements.define("ha-panel-states", PartialCards);

15
src/polymer-types.ts Normal file
View File

@ -0,0 +1,15 @@
// Force file to be a module to augment global scope.
export {};
declare global {
// for fire event
interface HASSDomEvents {
"iron-resize": undefined;
"config-refresh": undefined;
"ha-refresh-cloud-status": undefined;
"hass-more-info": {
entityId: string;
};
"location-changed": undefined;
}
}

View File

@ -1,6 +1,19 @@
import "@polymer/paper-styles/paper-styles";
import "@polymer/polymer/polymer-legacy";
export const buttonLink = `
button.link {
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
text-align: left;
text-decoration: underline;
cursor: pointer;
}
`;
const documentContainer = document.createElement("template");
documentContainer.setAttribute("style", "display: none;");
@ -162,16 +175,7 @@ documentContainer.innerHTML = `<custom-style>
@apply --paper-font-title;
}
button.link {
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
text-align: left;
text-decoration: underline;
cursor: pointer;
}
${buttonLink}
.card-actions a {
text-decoration: none;

View File

@ -7,6 +7,7 @@ import LocalizeMixin from "../mixins/localize-mixin";
import computeStateDisplay from "../common/entity/compute_state_display";
import attributeClassNames from "../common/entity/attribute_class_names";
import { computeRTL } from "../common/util/compute_rtl";
/*
* @appliesMixin LocalizeMixin
@ -21,6 +22,11 @@ class StateCardDisplay extends LocalizeMixin(PolymerElement) {
@apply --layout-baseline;
}
:host([rtl]) {
direction: rtl;
text-align: right;
}
state-info {
flex: 1 1 auto;
min-width: 0;
@ -33,6 +39,12 @@ class StateCardDisplay extends LocalizeMixin(PolymerElement) {
max-width: 40%;
flex: 0 0 auto;
}
:host([rtl]) .state {
margin-right: 16px;
margin-left: 0;
text-align: left;
}
.state.has-unit_of_measurement {
white-space: nowrap;
}
@ -63,6 +75,11 @@ class StateCardDisplay extends LocalizeMixin(PolymerElement) {
type: Boolean,
value: false,
},
rtl: {
type: Boolean,
reflectToAttribute: true,
computed: "_computeRTL(hass)",
},
};
}
@ -77,5 +94,9 @@ class StateCardDisplay extends LocalizeMixin(PolymerElement) {
];
return classes.join(" ");
}
_computeRTL(hass) {
return computeRTL(hass);
}
}
customElements.define("state-card-display", StateCardDisplay);

View File

@ -442,7 +442,8 @@
},
"common": {
"loading": "Loading",
"cancel": "Cancel"
"cancel": "Cancel",
"save": "Save"
},
"components": {
"entity": {
@ -767,10 +768,28 @@
}
},
"editor": {
"edit": {
"header": "Edit UI",
"configure_ui": "Configure UI",
"edit_view": {
"header": "View Configuration",
"add": "Add view",
"edit": "Edit view",
"delete": "Delete view"
},
"edit_card": {
"header": "Card Configuration",
"save": "Save",
"toggle_editor": "Toggle Editor"
"pick_card": "Pick the card you want to add.",
"toggle_editor": "Toggle Editor",
"add": "Add Card",
"edit": "Edit",
"delete": "Delete"
},
"save_config": {
"header": "Take control of your Lovelace UI",
"para": "By default Home Assistant will maintain your user interface, updating it when new entities or Lovelace components become available. If you take control we will no longer make changes automatically for you.",
"para_sure": "Are you sure you want to take control of your user interface?",
"cancel": "Never mind",
"save": "Take control"
},
"migrate": {
"header": "Configuration Incompatible",

View File

@ -15,6 +15,11 @@ declare global {
var __VERSION__: string;
}
export interface WebhookError {
code: number;
message: string;
}
export interface Credential {
auth_provider_type: string;
auth_provider_id: string;

View File

@ -1,3 +1,5 @@
import { supportsFeature } from "../common/entity/supports-feature";
/* eslint-enable no-bitwise */
export default class CoverEntity {
constructor(hass, stateObj) {
@ -37,38 +39,36 @@ export default class CoverEntity {
return this.stateObj.state === "closing";
}
/* eslint-disable no-bitwise */
get supportsOpen() {
return (this._feat & 1) !== 0;
return supportsFeature(this.stateObj, 1);
}
get supportsClose() {
return (this._feat & 2) !== 0;
return supportsFeature(this.stateObj, 2);
}
get supportsSetPosition() {
return (this._feat & 4) !== 0;
return supportsFeature(this.stateObj, 4);
}
get supportsStop() {
return (this._feat & 8) !== 0;
return supportsFeature(this.stateObj, 8);
}
get supportsOpenTilt() {
return (this._feat & 16) !== 0;
return supportsFeature(this.stateObj, 16);
}
get supportsCloseTilt() {
return (this._feat & 32) !== 0;
return supportsFeature(this.stateObj, 32);
}
get supportsStopTilt() {
return (this._feat & 64) !== 0;
return supportsFeature(this.stateObj, 64);
}
get supportsSetTiltPosition() {
return (this._feat & 128) !== 0;
return supportsFeature(this.stateObj, 128);
}
get isTiltOnly() {
@ -121,24 +121,22 @@ export default class CoverEntity {
}
}
const support = (stateObj, feature) =>
(stateObj.attributes.supported_features & feature) !== 0;
export const supportsOpen = (stateObj) => supportsFeature(stateObj, 1);
export const supportsOpen = (stateObj) => support(stateObj, 1);
export const supportsClose = (stateObj) => supportsFeature(stateObj, 2);
export const supportsClose = (stateObj) => support(stateObj, 2);
export const supportsSetPosition = (stateObj) => supportsFeature(stateObj, 4);
export const supportsSetPosition = (stateObj) => support(stateObj, 4);
export const supportsStop = (stateObj) => supportsFeature(stateObj, 8);
export const supportsStop = (stateObj) => support(stateObj, 8);
export const supportsOpenTilt = (stateObj) => supportsFeature(stateObj, 16);
export const supportsOpenTilt = (stateObj) => support(stateObj, 16);
export const supportsCloseTilt = (stateObj) => supportsFeature(stateObj, 32);
export const supportsCloseTilt = (stateObj) => support(stateObj, 32);
export const supportsStopTilt = (stateObj) => supportsFeature(stateObj, 64);
export const supportsStopTilt = (stateObj) => support(stateObj, 64);
export const supportsSetTiltPosition = (stateObj) => support(stateObj, 128);
export const supportsSetTiltPosition = (stateObj) =>
supportsFeature(stateObj, 128);
export function isTiltOnly(stateObj) {
const supportsCover =

View File

@ -1,3 +1,5 @@
import { supportsFeature } from "../common/entity/supports-feature";
export default class MediaPlayerEntity {
constructor(hass, stateObj) {
this.hass = hass;
@ -62,58 +64,54 @@ export default class MediaPlayerEntity {
return progress;
}
/* eslint-disable no-bitwise */
get supportsPause() {
return (this._feat & 1) !== 0;
return supportsFeature(this.stateObj, 1);
}
get supportsVolumeSet() {
return (this._feat & 4) !== 0;
return supportsFeature(this.stateObj, 4);
}
get supportsVolumeMute() {
return (this._feat & 8) !== 0;
return supportsFeature(this.stateObj, 8);
}
get supportsPreviousTrack() {
return (this._feat & 16) !== 0;
return supportsFeature(this.stateObj, 16);
}
get supportsNextTrack() {
return (this._feat & 32) !== 0;
return supportsFeature(this.stateObj, 32);
}
get supportsTurnOn() {
return (this._feat & 128) !== 0;
return supportsFeature(this.stateObj, 128);
}
get supportsTurnOff() {
return (this._feat & 256) !== 0;
return supportsFeature(this.stateObj, 256);
}
get supportsPlayMedia() {
return (this._feat & 512) !== 0;
return supportsFeature(this.stateObj, 512);
}
get supportsVolumeButtons() {
return (this._feat & 1024) !== 0;
return supportsFeature(this.stateObj, 1024);
}
get supportsSelectSource() {
return (this._feat & 2048) !== 0;
return supportsFeature(this.stateObj, 2048);
}
get supportsSelectSoundMode() {
return (this._feat & 65536) !== 0;
return supportsFeature(this.stateObj, 65536);
}
get supportsPlay() {
return (this._feat & 16384) !== 0;
return supportsFeature(this.stateObj, 16384);
}
/* eslint-enable no-bitwise */
get primaryTitle() {
return this._attr.media_title;
}

View File

@ -632,7 +632,7 @@
"abort_intro": "Входът е прекратен",
"form": {
"working": "Моля, изчакайте",
"unknown_error": "Ся си е*а майката",
"unknown_error": "Неочаквана грешка",
"providers": {
"homeassistant": {
"step": {

View File

@ -144,7 +144,8 @@
"performance": "Rendiment",
"high_demand": "Plena potència",
"heat_pump": "Bomba de calor",
"gas": "Gas"
"gas": "Gas",
"manual": "Manual"
},
"configurator": {
"configure": "Configurar",
@ -231,7 +232,7 @@
"default": {
"initializing": "Inicialitzant",
"dead": "No disponible",
"sleeping": "En espera",
"sleeping": "Dormint",
"ready": "A punt"
},
"query_stage": {
@ -296,11 +297,11 @@
"microphone_tip": "Toqueu el micròfon a la part superior dreta i dieu \"Add candy to my shopping list\""
},
"history": {
"showing_entries": "S'estan mostrant entrades per a",
"showing_entries": "Mostrant entrades de",
"period": "Període"
},
"logbook": {
"showing_entries": "S'estan mostrant entrades per a"
"showing_entries": "Mostrant entrades de"
},
"mailbox": {
"empty": "No teniu missatges",
@ -536,7 +537,7 @@
"caption": "Integracions",
"description": "Gestioneu dispositius i serveis connectats",
"discovered": "Descobert",
"configured": "Configurat",
"configured": "Configurades",
"new": "Configureu una nova integració",
"configure": "Configurar",
"none": "Encara no hi ha res configurat",
@ -712,6 +713,28 @@
"required_fields": "Ompliu tots els camps obligatoris"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "Elements seleccionats",
"clear_items": "Esborra els elements seleccionats",
"add_item": "Afegir element"
}
},
"editor": {
"edit_card": {
"header": "Targeta de Configuració",
"save": "Desa",
"toggle_editor": "Commuta l'editor"
},
"migrate": {
"header": "Configuració incompatible",
"para_no_id": "Aquest element no té ID. Afegiu un ID per aquest element a 'ui-lovelace.yaml'.",
"para_migrate": "Home Assistant pot afegir ID's a totes les vostres targetes i visualitzacions automàticament fent clic al botó \"Migrar la configuració\".",
"migrate": "Migrar la configuració"
}
}
}
},
"sidebar": {
@ -723,8 +746,8 @@
"cancel": "Cancel·lar"
},
"duration": {
"day": "{count} {count, plural,\none {dia}\nother {dies}\n}",
"week": "{count} {count, plural,\none {setmana}\nother {setmanes}\n}",
"day": "{count} {count, plural,\n one {dia}\n other {dies}\n}",
"week": "{count} {count, plural,\n one {setmana}\n other {setmanes}\n}",
"second": "{count} {count, plural,\none {segon}\nother {segons}\n}",
"minute": "{count} {count, plural,\none {minut}\nother {minuts}\n}",
"hour": "{count} {count, plural,\none {hora}\nother {hores}\n}"

View File

@ -144,7 +144,8 @@
"performance": "Vysoký výkon",
"high_demand": "Vysoký výkon",
"heat_pump": "Tepelné čerpadlo",
"gas": "Plyn"
"gas": "Plyn",
"manual": "Ruční"
},
"configurator": {
"configure": "Nakonfigurovat",
@ -578,7 +579,7 @@
"description": "Každý obnovovací token představuje relaci přihlášení. Aktualizační tokeny budou automaticky odstraněny po klepnutí na tlačítko Odhlásit. Následující tokeny obnovení jsou v současné době aktivní pro váš účet.",
"token_title": "Obnovit token pro {clientId}",
"created_at": "Vytvořeno {date}",
"confirm_delete": "Opravdu že chcete odstranit token pro {název}?",
"confirm_delete": "Opravdu že chcete odstranit token pro {name}?",
"delete_failed": "Nepodařilo se smazat token.",
"last_used": "Naposledy použito {date} z {location}",
"not_used": "Nikdy nebylo použito",
@ -712,6 +713,28 @@
"required_fields": "Vyplňte všechna povinná pole"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "Označené položky",
"clear_items": "Vymazat označené položky",
"add_item": "Přidat položku"
}
},
"editor": {
"edit_card": {
"header": "Konfigurace karty",
"save": "Uložit",
"toggle_editor": "Přepnout Editor"
},
"migrate": {
"header": "Konfigurace není kompatibilní",
"para_no_id": "Tento prvek nemá ID. Přidejte k tomuto prvku ID v 'ui-lovelace.yaml'.",
"para_migrate": "Home Assistant může automaticky přidávat ID ke všem kartám a pohledům stisknutím tlačítka Migrate config.",
"migrate": "Migrovat konfiguraci"
}
}
}
},
"sidebar": {

View File

@ -144,7 +144,8 @@
"performance": "Leistung",
"high_demand": "Hoher Verbrauch",
"heat_pump": "Wärmepumpe",
"gas": "Gas"
"gas": "Gas",
"manual": "Handbuch"
},
"configurator": {
"configure": "Konfigurieren",
@ -712,6 +713,25 @@
"required_fields": "Fülle alle Pflichtfelder aus."
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "Markierte Items",
"clear_items": "Markierte Elemente löschen",
"add_item": "Item hinzufügen"
}
},
"editor": {
"edit_card": {
"save": "Speichern",
"toggle_editor": "Editor umschalten"
},
"migrate": {
"header": "Konfiguration inkompatibel",
"para_no_id": "Dieses Element hat keine ID. Bitte füge diesem Element eine ID in 'ui-lovelace.yaml' hinzu."
}
}
}
},
"sidebar": {

View File

@ -144,7 +144,8 @@
"performance": "Performance",
"high_demand": "High demand",
"heat_pump": "Heat pump",
"gas": "Gas"
"gas": "Gas",
"manual": "Manual"
},
"configurator": {
"configure": "Configure",
@ -712,6 +713,28 @@
"required_fields": "Fill in all required fields"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "Checked items",
"clear_items": "Clear checked items",
"add_item": "Add item"
}
},
"editor": {
"edit_card": {
"header": "Card Configuration",
"save": "Save",
"toggle_editor": "Toggle Editor"
},
"migrate": {
"header": "Configuration Incompatible",
"para_no_id": "This element doesn't have an ID. Please add an ID to this element in 'ui-lovelace.yaml'.",
"para_migrate": "Home Assistant can add ID's to all your cards and views automatically for you by pressing the 'Migrate config' button.",
"migrate": "Migrate config"
}
}
}
},
"sidebar": {

View File

@ -144,7 +144,8 @@
"performance": "Rendimiento",
"high_demand": "Alta Demanda",
"heat_pump": "Bomba de Calor",
"gas": "Gas"
"gas": "Gas",
"manual": "Manual"
},
"configurator": {
"configure": "Configurar",
@ -712,6 +713,28 @@
"required_fields": "Completar todos los campos requeridos"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "lementos marcados",
"clear_items": "Borrar elementos marcados",
"add_item": "Agregar elemento"
}
},
"editor": {
"edit_card": {
"header": "Configuración de la tarjeta",
"save": "Guardar",
"toggle_editor": "Cambiar editor"
},
"migrate": {
"header": "Configuración inválida",
"para_no_id": "Este elemento no tiene un ID. Por favor agregue uno a este elemento en 'ui-lovelace.yaml'.",
"para_migrate": "Home Assistant puede agregar ID a todas sus tarjetas y vistas automáticamente por usted presionando el botón 'Migrar configuración'.",
"migrate": "Migrar configuración"
}
}
}
},
"sidebar": {

View File

@ -144,7 +144,8 @@
"performance": "Rendimiento",
"high_demand": "Alta demanda",
"heat_pump": "Bomba de calor",
"gas": "Gas"
"gas": "Gas",
"manual": "Manual"
},
"configurator": {
"configure": "Configurar",
@ -589,7 +590,7 @@
"description": "Crea tokens de acceso de larga duración para permitir que tus scripts interactúen con tu Home Assistant. Cada token será válido por 10 años desde la creación. Los siguientes tokens de acceso de larga duración están actualmente activos.",
"learn_auth_requests": "Aprenda a realizar solicitudes autenticadas.",
"created_at": "Creado el {date}",
"confirm_delete": "¿Está seguro de que desea eliminar el token de acceso para {nombre}?",
"confirm_delete": "¿Está seguro de que desea eliminar el token de acceso para {name}?",
"delete_failed": "Error al eliminar el token de acceso.",
"create": "Crear Token",
"create_failed": "No se ha podido crear el token de acceso.",
@ -712,6 +713,28 @@
"required_fields": "Complete todos los campos requeridos"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "Elementos marcados",
"clear_items": "Borrar elementos marcados",
"add_item": "Añadir artículo"
}
},
"editor": {
"edit_card": {
"header": "Configuración de la tarjeta",
"save": "Guardar",
"toggle_editor": "Alternar editor"
},
"migrate": {
"header": "Configuración incompatible",
"para_no_id": "Este elemento no tiene un ID. Por favor agregue uno este elemento en 'ui-lovelace.yaml'.",
"para_migrate": "Home Assistant puede agregar ID a todas sus tarjetas y vistas automáticamente por usted presionando el botón 'Migrar configuración'.",
"migrate": "Migrar configuración"
}
}
}
},
"sidebar": {

View File

@ -144,7 +144,8 @@
"performance": "Jõudlus",
"high_demand": "Kõrge nõudlus",
"heat_pump": "Soojuspump",
"gas": "Gaas"
"gas": "Gaas",
"manual": "Käsitsi"
},
"configurator": {
"configure": "Seadista",

View File

@ -144,7 +144,8 @@
"performance": "Tehokas",
"high_demand": "Täysi teho",
"heat_pump": "Lämpöpumppu",
"gas": "Kaasu"
"gas": "Kaasu",
"manual": "käsisäätöinen"
},
"configurator": {
"configure": "Määrittele",
@ -619,7 +620,8 @@
},
"page-authorize": {
"initializing": "Alustetaan",
"logging_in_with": "Kirjaudutaan sisään",
"authorizing_client": "Olet antamassa pääsyn {clientId} Home Assistant ympäristöösi.",
"logging_in_with": "Kirjaudutaan sisään **{authProviderName}**.",
"pick_auth_provider": "Tai kirjaudu sisään joillakin seuraavista",
"abort_intro": "Kirjautuminen on keskeytetty",
"form": {
@ -681,6 +683,9 @@
},
"description": "Valitse käyttäjä, jolla haluat kirjautua:"
}
},
"abort": {
"not_whitelisted": "Tietokonettasi ei ole sallittu."
}
}
}
@ -701,6 +706,28 @@
"required_fields": "Täytä kaikki pakolliset kentät"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "Valitut",
"clear_items": "Tyhjää valitut",
"add_item": "Lisää"
}
},
"editor": {
"edit_card": {
"header": "Kortti-asetukset",
"save": "Tallenna",
"toggle_editor": "Vaihda editori"
},
"migrate": {
"header": "Epäkelvot asetukset",
"para_no_id": "Elementillä ei ole ID. Lisää ID elementille 'ui-lovelace.yaml'-tiedostossa.",
"para_migrate": "Home Assistant voi lisätä ID:t kaikkiin kortteihisi ja näkymiin automaattisesti painamalla 'Tuo vanhat asetukset'-nappia.",
"migrate": "Tuo vanhat asetukset"
}
}
}
},
"sidebar": {
@ -769,7 +796,9 @@
"clear_code": "Tyhjennä",
"disarm": "Poista hälytys",
"arm_home": "Viritä (kotona)",
"arm_away": "Viritä (poissa)"
"arm_away": "Viritä (poissa)",
"arm_night": "Viritä yöksi",
"armed_custom_bypass": "Mukautettu ohitus"
},
"automation": {
"last_triggered": "Viimeksi käynnistetty",

View File

@ -144,7 +144,8 @@
"performance": "Performance",
"high_demand": "Forte demande",
"heat_pump": "Pompe à chaleur",
"gas": "Gaz"
"gas": "Gaz",
"manual": "Manuel"
},
"configurator": {
"configure": "Configurer",
@ -712,6 +713,26 @@
"required_fields": "Remplissez tous les champs requis"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "Éléments cochés",
"add_item": "Ajouter un élément"
}
},
"editor": {
"edit_card": {
"save": "Enregistrer",
"toggle_editor": "Activer\/désactiver léditeur"
},
"migrate": {
"header": "Configuration incompatible",
"para_no_id": "Cet élément n'a pas d'identifiant. Veuillez ajouter un identifiant à cet élément dans 'ui-lovelace.yaml'.",
"para_migrate": "Home Assistant peut ajouter automatiquement des identifiants à toutes vos cartes et vues en appuyant sur le bouton «Migrer la configuration».",
"migrate": "Migrer la configuration"
}
}
}
},
"sidebar": {
@ -849,7 +870,7 @@
"service": "Service"
},
"relative_time": {
"past": "{time} auparavant",
"past": "Il y a {time}",
"future": "Dans {time}",
"never": "Jamais",
"duration": {

View File

@ -348,6 +348,7 @@
},
"actions": {
"header": "Aktione",
"unsupported_action": "Nicht unterstützte Aktion: {action}",
"type": {
"delay": {
"label": "Vrzögerig"
@ -410,6 +411,13 @@
"mfa": {
"disable": "Deaktiviert"
}
},
"lovelace": {
"editor": {
"edit_card": {
"save": "Speichern"
}
}
}
},
"login-form": {

View File

@ -712,6 +712,20 @@
"required_fields": "מלא את כל השדות הדרושים"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "פריטים מסומנים",
"clear_items": "נקה פריטים מסומנים",
"add_item": "הוסף פריט"
}
},
"editor": {
"edit_card": {
"save": "שמור"
}
}
}
},
"sidebar": {

View File

@ -99,6 +99,17 @@
"off": "Zatvoreno",
"on": "Otvori"
},
"garage_door": {
"off": "Zatvoren",
"on": "Otvoreno"
},
"heat": {
"off": "Normalno"
},
"window": {
"off": "Zatvoreno",
"on": "Otvoreno"
},
"lock": {
"off": "Zaključano",
"on": "Otključano"
@ -127,7 +138,8 @@
"performance": "Performanse",
"high_demand": "Velike potražnje",
"heat_pump": "Toplinska pumpa",
"gas": "Plin"
"gas": "Plin",
"manual": "Priručnik"
},
"configurator": {
"configure": "Konfiguriranje",
@ -223,15 +235,27 @@
}
},
"weather": {
"clear-night": "Vedro, noć",
"cloudy": "Oblačno",
"fog": "Magla",
"lightning": "Munja",
"lightning-rainy": "Munja, kišna",
"partlycloudy": "Djelomično oblačno",
"pouring": "Lije",
"rainy": "Kišovito",
"snowy": "Snježno",
"snowy-rainy": "Snježno, kišno",
"sunny": "Sunčano",
"windy": "Vjetrovito"
"windy": "Vjetrovito",
"windy-variant": "Vjetrovito"
},
"vacuum": {
"cleaning": "Čišćenje",
"error": "Greška",
"idle": "Besposlen",
"off": "Ugašeno",
"on": "Upaljeno",
"paused": "Pauzirano"
}
},
"state_badge": {
@ -263,53 +287,209 @@
"add_item": "Dodaj stavku",
"microphone_tip": "Dodirnite mikrofon u gornjem desnom kutu i recite “Add candy to my shopping list”"
},
"history": {
"showing_entries": "Prikazivanje stavki za",
"period": "Razdoblje"
},
"logbook": {
"showing_entries": "Prikaži entitete za"
},
"mailbox": {
"empty": "Nemate nijednu poruku",
"playback_title": "Reprodukcija poruke",
"delete_prompt": "Izbrisati ovu poruku?",
"delete_button": "Izbrisati"
},
"config": {
"header": "Konfigurirajte Home Assistant",
"core": {
"caption": "Općenito",
"section": {
"core": {
"header": "Konfiguracija i kontrola poslužitelja"
"header": "Konfiguracija i kontrola poslužitelja",
"validation": {
"check_config": "Provjerite konfiguraciju",
"valid": "Konfiguracija valjana!",
"invalid": "Konfiguracija nije važeća"
},
"reloading": {
"core": "Ponovno učitati jezgru",
"group": "Ponovno učitajte grupe",
"automation": "Ponovo učitajte automatizacije"
},
"server_management": {
"heading": "Upravljanje poslužiteljem",
"introduction": "Kontrolirajte vaš Home Assistant server ... iz Home Assistant.",
"restart": "Ponovno pokretanje"
}
}
}
},
"zwave": {
"caption": "Z-Wave"
"customize": {
"description": "Prilagodite entitete"
},
"automation": {
"caption": "Automatizacija",
"description": "Stvaranje i uređivanje automatizacija",
"picker": {
"header": "Urednik Automatizacije "
"header": "Urednik Automatizacije ",
"introduction": "Automatizacijski urednik omogućuje stvaranje i uređivanje automatizacije. Molimo pročitajte [upute] (https:\/\/home-assistant.io\/docs\/automation\/editor\/) kako biste bili sigurni da ste ispravno konfigurirali Home Assistant.",
"pick_automation": "Odaberite automatizaciju za uređivanje",
"no_automations": "Nismo mogli pronaći nikakve automatizirane uređaje",
"add_automation": "Dodaj automatizaciju"
},
"editor": {
"introduction": "Upotrijebite automatizaciju kako bi vaš dom oživio",
"default_name": "Nova automatizacija",
"save": "Spremi",
"unsaved_confirm": "Imate nespremljene izmjene. Jeste li sigurni da želite napustiti?",
"alias": "Ime",
"triggers": {
"introduction": "Okidači su ono što pokreće obradu pravila o automatizaciji. Moguće je odrediti više okidača za isto pravilo. Kada pokrenete okidač, Home Assistant provjerit će uvjete, ako ih ima i pozvati akciju. \n\n [Saznajte više o pokretačima.] (Https:\/\/home-assistant.io\/docs\/automation\/trigger\/)",
"delete": "Obriši",
"unsupported_platform": "Nepodržana platforma: {platform}",
"type": {
"state": {
"from": "Od",
"for": "Za"
},
"homeassistant": {
"label": "Home Assistant",
"event": "Događaj:",
"start": "Početak",
"shutdown": "Ugasiti"
},
"mqtt": {
"label": "MQTT",
"topic": "Tema"
},
"numeric_state": {
"label": "Numeričko stanje",
"above": "Iznad",
"below": "Ispod",
"value_template": "Predložak vrijednosti (nije obavezno)"
},
"sun": {
"label": "Sunce",
"event": "Event:",
"sunrise": "Izlazak sunca",
"sunset": "Zalazak sunca",
"offset": "Offset (nije obavezno)"
},
"template": {
"label": "Predložak",
"value_template": "Predložak vrijednosti"
},
"time": {
"label": "Vrijeme",
"at": "U"
},
"zone": {
"label": "Zona",
"entity": "Entitet s lokacijom",
"zone": "Zona",
"event": "Event:",
"enter": "Unesite",
"leave": "Napustiti"
}
}
},
"conditions": {
"header": "Uvjeti",
"introduction": "Uvjeti su neobvezni dio pravila o automatizaciji i mogu se upotrebljavati kako bi se spriječilo da se neka akcija dogodi prilikom pokretanja. Uvjeti izgledaju vrlo slični pokretačima, ali su vrlo različiti. Okidač će pogledati događaje koji se događaju u sustavu, a stanje samo gleda kako sustav izgleda upravo sada. Okidač može primijetiti da je prekidač uključen. Stanje može vidjeti samo ako je uključen ili isključen prekidač. \n\n [Saznajte više o uvjetima.] (Https:\/\/home-assistant.io\/docs\/scripts\/conditions\/)",
"add": "Dodaj uvjet",
"duplicate": "Dupliciran",
"delete": "Obriši",
"delete_confirm": "Sigurno želite pobrisati?",
"unsupported_condition": "Nepodržano stanje: {condition}",
"type_select": "Vrsta uvjeta",
"type": {
"state": {
"label": "Stanje",
"state": "Stanje"
},
"numeric_state": {
"label": "Numeričko stanje",
"above": "Iznad",
"below": "Ispod",
"value_template": "Predložak vrijednosti (opciono)"
},
"sun": {
"label": "Sunce",
"before": "Prije:",
"after": "Nakon:",
"before_offset": "Prije pomaka (izborno)",
"after_offset": "Nakon pomaka (izborno)",
"sunrise": "Izlazak sunca",
"sunset": "Zalazak sunca"
},
"template": {
"label": "Predložak",
"value_template": "Predložak vrijednosti"
},
"time": {
"label": "Vrijeme",
"after": "Nakon",
"before": "Prije"
},
"zone": {
"label": "Zona",
"entity": "Entitet sa lokacijom",
"zone": "Zona"
}
}
},
"actions": {
"header": "Akcije",
"introduction": "Radnje su ono što će Home Assistant učiniti kada se aktivira automatizacija. \n\n [Saznajte više o radnjama.] (Https:\/\/home-assistant.io\/docs\/automation\/action\/)",
"add": "Dodajte radnju",
"duplicate": "Dupliciraj",
"delete": "Obriši",
"delete_confirm": "Sigurno želite pobrisati?",
"unsupported_action": "Nepodržana radnja: {action}",
"type_select": "Vrsta akcije",
"type": {
"service": {
"label": "Zovi servis",
"service_data": "Podaci o usluzi"
},
"delay": {
"label": "Odgoditi",
"delay": "Odgodi"
},
"wait_template": {
"label": "Čekaj",
"wait_template": "Čekaj predložak",
"timeout": "Vremensko ograničenje (opcionalno)"
},
"condition": {
"label": "Stanje"
},
"event": {
"label": "Pokreni događaj",
"event": "Event:",
"service_data": "Servisni podaci"
}
}
},
"triggers": {
"type": {
"state": {
"for": "Za"
}
}
}
}
},
"zwave": {
"caption": "Z-Wave"
},
"users": {
"caption": "Korisnici",
"description": "Upravljanje korisnicima"
"description": "Upravljanje korisnicima",
"picker": {
"title": "Korisnici"
},
"editor": {
"rename_user": "Preimenuj korisnika",
"change_password": "Promijeni lozinku",
"activate_user": "Aktivirajte korisnika",
"deactivate_user": "Deaktivirajte korisnika",
"delete_user": "Izbriši korisnika"
}
},
"cloud": {
"caption": "Home Assistant Cloud",
@ -422,7 +602,8 @@
"step": {
"init": {
"data": {
"username": "Korisničko ime"
"username": "Korisničko ime",
"password": "Lozinka"
}
},
"mfa": {
@ -433,6 +614,7 @@
}
},
"error": {
"invalid_auth": "Neispravno korisničko ime ili lozinka",
"invalid_code": "Pogrešan kod za provjeru autentičnosti "
},
"abort": {
@ -442,26 +624,49 @@
"legacy_api_password": {
"step": {
"init": {
"data": {
"password": "API lozinka"
},
"description": "Unesite API lozinku u svoj http config:"
},
"mfa": {
"data": {
"code": "Kôd autentifikacije s dva faktora"
}
}
},
"error": {
"invalid_auth": "Nevažeća lozinka za API",
"invalid_code": "Nevažeći kôd za autentifikaciju"
},
"abort": {
"no_api_password_set": "Nemate konfiguriranu lozinku za API.",
"login_expired": "Sesija istekla, molim vas prijavite se ponovno."
}
},
"trusted_networks": {
"step": {
"init": {
"data": {
"user": "Korisnik"
},
"description": "Odaberite korisnika kojim se želite prijaviti:"
}
},
"abort": {
"not_whitelisted": "Računalo nije na popisu dopuštenih."
}
}
}
}
},
"page-onboarding": {
"intro": "Jeste li spremni probuditi svoj dom, vratiti svoju privatnost i pridružiti se svjetskoj zajednici tinkerera?",
"user": {
"intro": "Započnimo stvaranjem korisničkog računa.",
"required_field": "Potreban",
"data": {
"name": "Ime",
"username": "Korisničko ime",
"password": "Lozinka"
},
@ -470,18 +675,52 @@
"required_fields": "Ispunite sva potrebna polja"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "Označene stavke",
"clear_items": "Izbrišite označene stavke",
"add_item": "Dodaj stavku"
}
},
"editor": {
"edit_card": {
"header": "Konfiguracija Kartice ",
"save": "Spremi",
"toggle_editor": "Uključi uređivač"
},
"migrate": {
"header": "Konfiguracija nije kompatibilna",
"para_no_id": "Ovaj element nema ID. Dodajte ID ovom elementu u 'ui-lovelace.yaml'.",
"para_migrate": "Home Assistant može dodati ID-ove na sve vaše kartice i pogleda automatski za vas pritiskom na gumb 'Migriraj konfiguriranje'.",
"migrate": "Migriraj konfiguraciju"
}
}
}
},
"sidebar": {
"log_out": "Odjava",
"developer_tools": "Razvojni alati"
},
"common": {
"loading": "Učitavam",
"cancel": "Otkazati"
},
"duration": {
"day": "{count} {count, plural,\n one {dan}\n other {dani}\n}",
"week": "{count} {count, plural,\n one {tjedan}\n other {tjedni}\n}",
"second": "{count} {count, plural,\n one {sekunde}\n other {sekunde}\n}"
"second": "{count} {count, plural,\n one {sekunde}\n other {sekunde}\n}",
"minute": "{broj} {broj, množina, jedna {minuta} druge {minute}}",
"hour": "{broj} {broj, množina, jedan {sat} drugi {sati}}"
},
"login-form": {
"password": "Lozinka"
},
"card": {
"camera": {
"not_available": "Slika nije dostupna"
},
"scene": {
"activate": "Aktivirati"
},
@ -497,14 +736,24 @@
"wind_speed": "Brzina vjetra"
},
"cardinal_direction": {
"n": "N"
"ese": "ESE",
"n": "N",
"ne": "NE",
"nne": "NNE",
"nw": "NW"
}
},
"alarm_control_panel": {
"code": "Kod"
"code": "Kod",
"disarm": "Deaktiviraj",
"arm_home": "Aktiviran doma",
"arm_away": "Aktiviran odsutno",
"arm_night": "Aktiviran nočni",
"armed_custom_bypass": "Prilagođena obilaznica"
},
"fan": {
"speed": "Brzina",
"oscillate": "Oscilirati",
"direction": "Smjer"
},
"light": {
@ -549,6 +798,14 @@
}
},
"components": {
"entity": {
"entity-picker": {
"entity": "Entitet"
}
},
"relative_time": {
"past": "{vrijeme} prije"
},
"history_charts": {
"loading_history": "Učitavanje povijesti stanja ...",
"no_history_found": "Nije pronađena povijest stanja."
@ -607,5 +864,12 @@
"updater": "Ažuriranje",
"weblink": "WebLink",
"zwave": "Z-Wave"
},
"attribute": {
"weather": {
"humidity": "Vlažnost",
"visibility": "Vidljivost",
"wind_speed": "Brzina vjetra"
}
}
}

View File

@ -144,7 +144,8 @@
"performance": "Teljesítmény",
"high_demand": "Magas igénybevétel",
"heat_pump": "Hőszivattyú",
"gas": "Gáz"
"gas": "Gáz",
"manual": "Manuális"
},
"configurator": {
"configure": "Beállítás",
@ -599,7 +600,7 @@
"last_used": "Utolsó használat ideje: {date}, helye: {location}",
"not_used": "Sosem használt"
},
"current_user": "Jelenleg bejelentkezve mint {fullName}.",
"current_user": "Jelenleg {fullName} felhasználóként vagy bejelentkezve.",
"is_owner": "Tulajdonos vagy.",
"logout": "Kijelentkezés",
"change_password": {
@ -712,6 +713,28 @@
"required_fields": "Töltsd ki az összes szükséges mezőt"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "Kijelölt tételek",
"clear_items": "Kijelölt tételek törlése",
"add_item": "Tétel hozzáadása"
}
},
"editor": {
"edit_card": {
"header": "Kártya Konfiguráció",
"save": "Mentés",
"toggle_editor": "Szerkesztő"
},
"migrate": {
"header": "Inkompatibilis Konfiguráció",
"para_no_id": "Ez az elem nem rendelkezik azonosítóval. Kérlek, adj hozzá egyet az 'ui-lovelace.yaml' fájlban!",
"para_migrate": "A Home Assistant a 'Konfiguráció áttelepítése' gomb megnyomásával az összes kártyához és nézethez automatikusan létre tud hozni azonosítókat.",
"migrate": "Konfiguráció áttelepítése"
}
}
}
},
"sidebar": {

View File

@ -144,7 +144,8 @@
"performance": "Prestazioni",
"high_demand": "Forte richiesta",
"heat_pump": "Pompa di calore",
"gas": "Gas"
"gas": "Gas",
"manual": "Manuale"
},
"configurator": {
"configure": "Configura",
@ -545,9 +546,9 @@
"no_device": "Entità senza dispositivi",
"delete_confirm": "Sei sicuro di voler eliminare questa integrazione?",
"restart_confirm": "Riavvia Home Assistant per terminare la rimozione di questa integrazione",
"manuf": "da {produttore}",
"manuf": "da {manufacturer}",
"hub": "Connesso via",
"firmware": "Firmware: {versione}",
"firmware": "Firmware: {version}",
"device_unavailable": "dispositivo non disponibile",
"entity_unavailable": "entità non disponibile"
}
@ -712,6 +713,28 @@
"required_fields": "Compila tutti i campi richiesti"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "Elementi selezionati",
"clear_items": "Cancella gli elementi selezionati",
"add_item": "Aggiungi elemento"
}
},
"editor": {
"edit_card": {
"header": "Configurazione della scheda",
"save": "Salva",
"toggle_editor": "Attiva \/ disattiva l'editor"
},
"migrate": {
"header": "Configurazione incompatibile",
"para_no_id": "Questo elemento non ha un ID. Aggiungi un ID a questo elemento in 'ui-lovelace.yaml'.",
"para_migrate": "Home Assistant può aggiungere automaticamente gli ID a tutte le tue schede e visualizzazioni automaticamente premendo il pulsante \"Eporta configurazione\".",
"migrate": "Esporta configurazione"
}
}
}
},
"sidebar": {
@ -794,7 +817,7 @@
},
"fan": {
"speed": "Velocità",
"oscillate": "Oscillante",
"oscillate": "Oscillazione",
"direction": "Direzione"
},
"light": {
@ -836,9 +859,9 @@
"water_heater": {
"currently": "Attualmente",
"on_off": "Acceso \/ Spento",
"target_temperature": "Temperatura di riferimento",
"target_temperature": "Temperatura da raggiungere",
"operation": "Operazione",
"away_mode": "Modalità Assente"
"away_mode": "Modalità fuori casa"
}
},
"components": {

View File

@ -144,7 +144,8 @@
"performance": "고효율",
"high_demand": "고성능",
"heat_pump": "순환펌프",
"gas": "가스"
"gas": "가스",
"manual": "수동"
},
"configurator": {
"configure": "설정",
@ -364,7 +365,7 @@
"alias": "이름",
"triggers": {
"header": "트리거",
"introduction": "트리거는 자동화 규칙을 처리하는 시작점 입니다. 같은 자동화 규칙에 여러 개의 트리거를 지정할 수 있습니다. 트리거가 발동되면 Home Assistant는 조건을 확인하고 동작을 호출합니다. \n\n [트리거에 대해 자세히 알아보기](https:\/\/home-assistant.io\/docs\/automation\/trigger\/)",
"introduction": "트리거는 자동화 규칙을 처리하는 시작점 입니다. 같은 자동화 규칙에 여러 개의 트리거를 지정할 수 있습니다. 트리거가 발동되면 Home Assistant는 조건을 확인하고 동작을 호출합니다. \n\n[트리거에 대해 자세히 알아보기](https:\/\/home-assistant.io\/docs\/automation\/trigger\/)",
"add": "트리거 추가",
"duplicate": "복제",
"delete": "삭제",
@ -427,7 +428,7 @@
},
"conditions": {
"header": "조건",
"introduction": "조건은 자동화 규칙의 선택사항이며 트리거 될 때 발생하는 동작을 방지하는 데 사용할 수 있습니다. 조건은 트리거와 비슷해 보이지만 매우 다릅니다. 트리거는 시스템에서 발생하는 이벤트를 검사하고 조건은 시스템의 현재 상태를 검사합니다. 스위치를 예로 들면, 트리거는 스위치가 켜지는 것(이벤트)을, 조건은 스위치가 현재 켜져 있는지 혹은 꺼져 있는지(상태)를 검사합니다. \n\n [조건에 대해 자세히 알아보기](https:\/\/home-assistant.io\/docs\/scripts\/conditions\/)",
"introduction": "조건은 자동화 규칙의 선택사항이며 트리거 될 때 발생하는 동작을 방지하는 데 사용할 수 있습니다. 조건은 트리거와 비슷해 보이지만 매우 다릅니다. 트리거는 시스템에서 발생하는 이벤트를 검사하고 조건은 시스템의 현재 상태를 검사합니다. 스위치를 예로 들면, 트리거는 스위치가 켜지는 것(이벤트)을, 조건은 스위치가 현재 켜져 있는지 혹은 꺼져 있는지(상태)를 검사합니다. \n\n[조건에 대해 자세히 알아보기](https:\/\/home-assistant.io\/docs\/scripts\/conditions\/)",
"add": "조건 추가",
"duplicate": "복제",
"delete": "삭제",
@ -712,6 +713,28 @@
"required_fields": "필수 입력란을 모두 채워주세요"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "선택한 항목",
"clear_items": "선택한 항목 삭제",
"add_item": "항목 추가"
}
},
"editor": {
"edit_card": {
"header": "카드 구성",
"save": "저장하기",
"toggle_editor": "에디터 전환"
},
"migrate": {
"header": "설정이 호환되지 않습니다",
"para_no_id": "이 구성요소에는 ID가 없습니다. 'ui-lovelace.yaml' 에 구성요소의 ID를 추가해주세요.",
"para_migrate": "Home Assistant 는 '설정 마이그레이션' 버튼을 눌러 자동으로 모든 카드와 보기에 ID를 추가 할 수 있습니다.",
"migrate": "설정 마이그레이션"
}
}
}
},
"sidebar": {

View File

@ -144,7 +144,8 @@
"performance": "Leeschtung",
"high_demand": "Héich Ufro",
"heat_pump": "Heizung",
"gas": "Gas"
"gas": "Gas",
"manual": "Manuell"
},
"configurator": {
"configure": "Astellen",
@ -574,30 +575,30 @@
"dropdown_label": "Thema"
},
"refresh_tokens": {
"header": "Tokeny Odświeżenia",
"header": "Token erneieren",
"description": "All Sessioun Token representéiert eng Login Sessioun. Sessioun's Token ginn automatesch gelëscht wann dir op auslogge klickt. Folgend Sessioun's Token si fir de Moment fir Ären Account aktiv.",
"token_title": "Token odświeżenia dla {clientId}",
"created_at": "Utworzono {date}",
"confirm_delete": "Jesteś pewien że chcesz usunąć token odświeżenia dla {name}?",
"delete_failed": "Nie udało się usunąć tokena odświeżenia.",
"last_used": "Ostatnio użyty {date} z miejsca {location}",
"not_used": "Nigdy nie został użyty",
"current_token_tooltip": "Nie można usunąć obecnego tokena odświeżenia."
"token_title": "Token erneiren fir {clientId}",
"created_at": "Erstallt um {date}",
"confirm_delete": "Sécher fir den Erneierungs Token fir {name} ze läsche?",
"delete_failed": "Fehler beim läschen vum Erneierungs Token",
"last_used": "Läscht benotz um {date} vun {location}",
"not_used": "Nach nie benotzt ginn",
"current_token_tooltip": "Fehler beim läschen vum aktuellen Erneierungs Token"
},
"long_lived_access_tokens": {
"header": "Lang gëlteg Access Token",
"description": "Erstellt laang gëlteg Access Token déi et äre Skripten erlabe mat ärem Home Assistant z'interagéieren. All eenzelen Token ass gëlteg fir 10 Joer. Folgend Access Token sinn am Moment aktiv.",
"learn_auth_requests": "Naucz się robić uwierzytelnione żądania.",
"created_at": "Utworzono {date}",
"confirm_delete": "Jesteś pewien że chcesz usunąć token dostępu dla {name}?",
"delete_failed": "Nie udało się usunąć tokena dostępu.",
"create": "Utwórz Token",
"create_failed": "Nie udało się utworzyć tokena dostępu.",
"prompt_name": "Nazwa?",
"prompt_copy_token": "Zapisz swój token dostępu. Nie zostanie więcej pokazany.",
"empty_state": "Nie posiadasz jeszcze długoterminowych tokenów dostępu.",
"last_used": "Ostatnio użyty {date} z miejsca {location}",
"not_used": "Nigdy nie został użyty"
"learn_auth_requests": "Leiert wéi een \"authenticated requests\" erstellt.",
"created_at": "Erstallt um {date}",
"confirm_delete": "Sécher fir den Access Token fir {name} ze läsche?",
"delete_failed": "Fehler beim läschen vum Access Token",
"create": "Token erstellen",
"create_failed": "Fehler beim erstellen vum Access Token",
"prompt_name": "Numm?",
"prompt_copy_token": "Kopéiert den Access Token. E gëtt nie méi ugewisen.",
"empty_state": "Et ginn nach keng laang gëlteg Access Token.",
"last_used": "Fir d'Läscht benotzt um {date} vun {location}",
"not_used": "Nach nie benotzt ginn"
},
"current_user": "Dir sidd aktuell ageloggt als {fullName}.",
"is_owner": "Dir sidd Proprietär.",
@ -712,6 +713,28 @@
"required_fields": "Fëllt all néideg Felder aus"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "Markéiert Elementer",
"clear_items": "Markéiert Elementer läschen",
"add_item": "Element dobäisetzen"
}
},
"editor": {
"edit_card": {
"header": "Kaart Konfiguratioun",
"save": "Späicheren",
"toggle_editor": "Editeur ëmschalten"
},
"migrate": {
"header": "Konfiguratioun net kompatibel",
"para_no_id": "Dëst Element huet keng ID. Definéiert w.e.g. eng ID fir dëst Element am 'ui-lovelace.yaml'.",
"para_migrate": "Home Assistant kann ID's zu all äre Kaarten automatesch dobäisetzen andeem dir de Knäppche 'Konfiguratioun migréieren' dréckt.",
"migrate": "Konfiguratioun migréieren"
}
}
}
},
"sidebar": {

View File

@ -144,7 +144,8 @@
"performance": "Prestatie",
"high_demand": "Hoge vraag",
"heat_pump": "Warmtepomp",
"gas": "Gas"
"gas": "Gas",
"manual": "Handmatig"
},
"configurator": {
"configure": "Configureer",
@ -712,6 +713,28 @@
"required_fields": "Vul alle verplichte velden in"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "Geselecteerde items",
"clear_items": "Geselecteerde items wissen",
"add_item": "Item toevoegen"
}
},
"editor": {
"edit_card": {
"header": "Kaart configuratie",
"save": "Opslaan",
"toggle_editor": "Toggle Editor"
},
"migrate": {
"header": "Configuratie incompatibel",
"para_no_id": "Dit element heeft geen ID. Voeg een ID toe aan dit element in 'ui-lovelace.yaml'.",
"para_migrate": "Home Assistant kan ID's voor al je kaarten en weergaven automatisch voor je toevoegen door op de knop 'Migrate config' te klikken.",
"migrate": "Configuratie migreren"
}
}
}
},
"sidebar": {
@ -726,7 +749,7 @@
"day": "{count} {count, plural,\none {dag}\nother {dagen}\n}",
"week": "{count} {count, plural,\none {week}\nother {weken}\n}",
"second": "{count} {count, plural,\none {seconde}\nother {seconden}\n}",
"minute": "{count} {count, plural,\néén {minuut}\nandere {minuten}\n}",
"minute": "{count} {count, plural,\none {minuut}\nother {minuten}\n}",
"hour": "{count} {count, plural,\none {uur}\nother {uur}\n}"
},
"login-form": {

View File

@ -477,7 +477,7 @@
"duplicate": "Dupliser",
"delete": "Slett",
"delete_confirm": "Er du sikker på at du vil slette?",
"unsupported_action": "Ikkjestøtta handling: {action}",
"unsupported_action": "Ikkje støtta handling: {action}",
"type_select": "Handlingstype",
"type": {
"service": {

View File

@ -144,7 +144,8 @@
"performance": "Ytelse",
"high_demand": "Høy etterspørsel",
"heat_pump": "Varmepumpe",
"gas": "Gass"
"gas": "Gass",
"manual": "Manuell"
},
"configurator": {
"configure": "Konfigurer",
@ -712,6 +713,28 @@
"required_fields": "Fyll ut alle nødvendige felt"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "Merkede elementer",
"clear_items": "Fjern merkede elementer",
"add_item": "Legg til"
}
},
"editor": {
"edit_card": {
"header": "Kortkonfigurasjon",
"save": "Lagre",
"toggle_editor": "Bytt redigering"
},
"migrate": {
"header": "Konfigurasjon inkompatibel",
"para_no_id": "Dette elementet har ingen ID. Vennligst legg til en ID til dette elementet i 'ui-lovelace.yaml'.",
"para_migrate": "Home Assistant kan legge til ID-er til alle dine kort og visninger automatisk for deg ved å trykke på 'Overfør konfigurasjon' knappen.",
"migrate": "Overfør konfigurasjon"
}
}
}
},
"sidebar": {

View File

@ -45,8 +45,8 @@
"on": "włączony"
},
"moisture": {
"off": "sucho",
"on": "wilgotno"
"off": "brak wilgoci",
"on": "wilgoć"
},
"gas": {
"off": "brak",
@ -144,7 +144,8 @@
"performance": "wydajność",
"high_demand": "duży rozbiór",
"heat_pump": "pompa ciepła",
"gas": "gaz"
"gas": "gaz",
"manual": "manualnie"
},
"configurator": {
"configure": "Skonfiguruj",
@ -330,7 +331,7 @@
"introduction": "Niektóre części Home Assistant'a można przeładować bez konieczności ponownego uruchomienia. Kliknięcie przeładuj spowoduje ponowne wczytanie konfiguracji.",
"core": "Przeładuj rdzeń",
"group": "Przeładuj grupy",
"automation": "Przeładuj reguły automatyzacji",
"automation": "Przeładuj automatyzacje",
"script": "Przeładuj skrypty"
},
"server_management": {
@ -386,8 +387,8 @@
"homeassistant": {
"label": "Home Assistant",
"event": "Zdarzenie:",
"start": "Początek",
"shutdown": "Zamknięcie systemu"
"start": "uruchomienie",
"shutdown": "zakończenie"
},
"mqtt": {
"label": "MQTT",
@ -403,8 +404,8 @@
"sun": {
"label": "Słońce",
"event": "Zdarzenie:",
"sunrise": "Wschód słońca",
"sunset": "Zachód słońca",
"sunrise": "wschód słońca",
"sunset": "zachód słońca",
"offset": "Przesunięcie (opcjonalnie)"
},
"template": {
@ -451,8 +452,8 @@
"after": "Po:",
"before_offset": "Przed przesunięciem (opcjonalnie)",
"after_offset": "Po przesunięciu (opcjonalnie)",
"sunrise": "Wschód słońca",
"sunset": "Zachód słońca"
"sunrise": "wschód słońca",
"sunset": "zachód słońca"
},
"template": {
"label": "Szablon",
@ -712,6 +713,28 @@
"required_fields": "Wypełnij wszystkie wymagane pola"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "Pozycje zaznaczone",
"clear_items": "Wyczyść zaznaczone pozycje",
"add_item": "Dodaj pozycję"
}
},
"editor": {
"edit_card": {
"header": "Konfiguracja karty",
"save": "Zapisz",
"toggle_editor": "Przełącz edytor"
},
"migrate": {
"header": "Konfiguracja niekompatybilna",
"para_no_id": "Ten element nie ma ID. Dodaj ID do tego elementu w \"ui-lovelace.yaml\".",
"para_migrate": "Home Assistant może automatycznie dodać ID do wszystkich twoich kart i widoków, po kliknięciu przycisku \"Migracja konfiguracji\".",
"migrate": "Migracja konfiguracji"
}
}
}
},
"sidebar": {
@ -745,7 +768,7 @@
"activate": "Aktywuj"
},
"script": {
"execute": "Wykonaj"
"execute": "Uruchom"
},
"weather": {
"attributes": {

View File

@ -144,7 +144,8 @@
"performance": "Desempenho",
"high_demand": "Alta demanda",
"heat_pump": "Bomba de calor",
"gas": "Gás"
"gas": "Gás",
"manual": "Manual"
},
"configurator": {
"configure": "Configurar",
@ -580,7 +581,7 @@
"created_at": "Criado em {date}",
"confirm_delete": "Tem certeza de que deseja excluir o token de acesso para {name} ?",
"delete_failed": "Falha ao excluir o token de acesso.",
"last_used": "Usado pela última vez em {data} em {localização}",
"last_used": "Usado pela última vez em {date} em {location}",
"not_used": "Nunca foi usado",
"current_token_tooltip": "Não é possível excluir o token de atualização atual"
},
@ -596,7 +597,7 @@
"prompt_name": "Nome?",
"prompt_copy_token": "Copie seu token de acesso. Ele não será mostrado novamente.",
"empty_state": "Você ainda não tem tokens de acesso de longa duração.",
"last_used": "Usado pela última vez em {data} em {localização}",
"last_used": "Usado pela última vez em {date} em {location}",
"not_used": "Nunca foi usado"
},
"current_user": "Você está logado como {fullName} .",
@ -712,6 +713,28 @@
"required_fields": "Preencha todos os campos obrigatórios"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "Itens marcados",
"clear_items": "Limpar itens marcados",
"add_item": "Adicionar item"
}
},
"editor": {
"edit_card": {
"header": "Configuração de cartão",
"save": "Salvar",
"toggle_editor": "Alternar Editor"
},
"migrate": {
"header": "Configuração Incompatível",
"para_no_id": "Este elemento não possui um ID. Por favor adicione um ID a este elemento em 'ui-lovelace.yaml'.",
"para_migrate": "O Home Assistant pode adicionar IDs a todos os seus cards e visualizações automaticamente clicando no botão 'Migrar config'.",
"migrate": "Migrar configuração"
}
}
}
},
"sidebar": {
@ -901,7 +924,7 @@
"climate": "Clima",
"configurator": "Configurador",
"conversation": "Conversação",
"cover": "Cortina",
"cover": "Cobertura",
"device_tracker": "Rastreador de dispositivo",
"fan": "Ventilador",
"history_graph": "Gráfico de histórico",

View File

@ -144,7 +144,8 @@
"performance": "Desempenho",
"high_demand": "Necessidade alta",
"heat_pump": "Bomba de calor",
"gas": "Gás"
"gas": "Gás",
"manual": "Manual"
},
"configurator": {
"configure": "Configurar",
@ -712,6 +713,28 @@
"required_fields": "Preencha todos os campos obrigatórios"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "Itens marcados",
"clear_items": "Limpar itens marcados",
"add_item": "Adicionar Item"
}
},
"editor": {
"edit_card": {
"header": "Configuração do cartão",
"save": "Guardar",
"toggle_editor": "Alterar para editor"
},
"migrate": {
"header": "Configuração Incompatível",
"para_no_id": "Este elemento não possui um ID. Por favor adicione um ID a este elemento em 'ui-lovelace.yaml'.",
"para_migrate": "O Home Assistant pode adicionar IDs a todos os seus cartões e vistas automaticamente clicando no botão 'Migrar configuração'.",
"migrate": "Migrar configuração"
}
}
}
},
"sidebar": {

View File

@ -369,7 +369,7 @@
"duplicate": "Dublura",
"delete": "Ștergeți",
"delete_confirm": "Sigur doriți să ștergeți?",
"unsupported_platform": "Platformă neacceptată: {platforma}",
"unsupported_platform": "Platformă neacceptată: {platform}",
"type_select": "Tip acțiune",
"type": {
"event": {
@ -577,8 +577,8 @@
"header": "Tokenuri de innoire",
"description": "Fiecare token de innoire reprezinta o sesiune de logare. Tokenuriile de innoire vor fi inlaturate cand se da click pe log out. Urmatoarele tokenuri de innoire sunt active in contul dumneavoastra.",
"token_title": "înnoirea token pentru {clientId}",
"created_at": "Creat in {data}",
"confirm_delete": "Sigur doriti sa stergeti tokenul de acces pentru {nume}?",
"created_at": "Creat in {date}",
"confirm_delete": "Sigur doriti sa stergeti tokenul de acces pentru {name}?",
"delete_failed": "Ştergerea tokenului de acces eşuată",
"last_used": "Ultima utilizate la {data} din {locaţie}",
"not_used": "Nu a fost utilizat niciodata",
@ -588,8 +588,8 @@
"header": "Tokenuri de acces de lunga durata",
"description": "Creați tokenuri de acces cu durată lungă de viață pentru a permite script-urilor dvs. să interacționeze cu instanța dvs. Home Assistant. Fiecare token va fi valabil timp de 10 ani de la creatie. Următoarele tokenuri de acces de lungă durată sunt active la ora actuala.",
"learn_auth_requests": "Aflați cum să faceți cereri autentificate.",
"created_at": "Creat in {data}",
"confirm_delete": "Sigur doriti sa stergeti tokenul de acces pentru {nume}?",
"created_at": "Creat in {date}",
"confirm_delete": "Sigur doriti sa stergeti tokenul de acces pentru {name}?",
"delete_failed": "Ştergerea tokenului de acces eşuată",
"create": "Creaza un Token",
"create_failed": "Crearea tokenului de acces eşuată",
@ -726,8 +726,8 @@
"day": "{count}{count, plural,\n one { zi }\n other { zile }\n}",
"week": "{count}{count, plural,\n one { săptămână }\n other { săptămâni }\n}",
"second": "{count} {count, plural,\none {secunda}\nother {secunde}\n}",
"minute": "{count}{count, plural,\n un { minute }\n alte { minutes }\n}",
"hour": "{count}{count, plural,\n o { hour }\n alte { hours }\n}"
"minute": "{count} {count, plural,\n one { minute }\n other { minutes }\n}",
"hour": "{count}{count, plural,\n one { zi }\n other { zile }\n}"
},
"login-form": {
"password": "Parola",

View File

@ -36,13 +36,13 @@
"armed_custom_bypass": "Охрана с исключениями"
},
"automation": {
"off": "Выключено",
"on": "Включено"
"off": "Выкл",
"on": "Вкл"
},
"binary_sensor": {
"default": {
"off": "Выключено",
"on": "Включено"
"off": "Выкл",
"on": "Вкл"
},
"moisture": {
"off": "Сухо",
@ -101,12 +101,12 @@
"on": "Охлаждение"
},
"door": {
"off": "Закрыта",
"on": "Открыта"
"off": "Закрыто",
"on": "Открыто"
},
"garage_door": {
"off": "Закрыта",
"on": "Открыта"
"off": "Закрыто",
"on": "Открыто"
},
"heat": {
"off": "Норма",
@ -122,8 +122,8 @@
}
},
"calendar": {
"off": "Выключено",
"on": "Включено"
"off": "Выкл",
"on": "Вкл"
},
"camera": {
"recording": "Запись",
@ -131,9 +131,9 @@
"idle": "Бездействие"
},
"climate": {
"off": "Выключено",
"on": "Включено",
"heat": "Обогрев",
"off": "Выкл",
"on": "Вкл",
"heat": "Нагрев",
"cool": "Охлаждение",
"idle": "Бездействие",
"auto": "Авто",
@ -144,40 +144,41 @@
"performance": "Производительность",
"high_demand": "Большая нагрузка",
"heat_pump": "Тепловой насос",
"gas": "Газовый"
"gas": "Газовый",
"manual": "Ручной режим"
},
"configurator": {
"configure": "Настроить",
"configured": "Настроено"
},
"cover": {
"open": "Открыта",
"open": "Открыто",
"opening": "Открывается",
"closed": "Закрыта",
"closed": "Закрыто",
"closing": "Закрывается",
"stopped": "Остановлена"
"stopped": "Остановлено"
},
"device_tracker": {
"home": "Дома",
"not_home": "Отсутствует"
},
"fan": {
"off": "Выключен",
"on": "Включен"
"off": "Выкл",
"on": "Вкл"
},
"group": {
"off": "Выключено",
"on": "Включено",
"off": "Выкл",
"on": "Вкл",
"home": "Дома",
"not_home": "Отсутствует",
"open": "Открыто",
"opening": "Открывается",
"closed": "Закрыто",
"closing": "Закрывается",
"stopped": "Остановлена",
"stopped": "Остановлено",
"locked": "Заблокирована",
"unlocked": "Разблокирована",
"ok": "OK",
"ok": "ОК",
"problem": "Проблема"
},
"input_boolean": {
@ -212,12 +213,12 @@
"scening": "Переход к сцене"
},
"script": {
"off": "Выключен",
"on": "Включен"
"off": "Выкл",
"on": "Вкл"
},
"sensor": {
"off": "Выключен",
"on": "Включен"
"off": "Выкл",
"on": "Вкл"
},
"sun": {
"above_horizon": "Над горизонтом",
@ -236,7 +237,7 @@
},
"query_stage": {
"initializing": "Инициализация ({query_stage})",
"dead": "Неисправен ({query_stage})"
"dead": "Неисправно ({query_stage})"
}
},
"weather": {
@ -281,7 +282,7 @@
"arming": "Постановка на охрану",
"disarming": "Снятие с охраны",
"triggered": "Тревога",
"armed_custom_bypass": "Под охраной"
"armed_custom_bypass": "Охрана"
},
"device_tracker": {
"home": "Дома",
@ -489,7 +490,7 @@
"delay": "Задержка"
},
"wait_template": {
"label": "Ожидать",
"label": "Ожидание",
"wait_template": "Шаблон ожидания",
"timeout": "Тайм-аут (опционально)"
},
@ -541,11 +542,11 @@
"configure": "Настроить",
"none": "Пока ничего не настроено",
"config_entry": {
"no_devices": "Эта интеграция не имеет устройств.",
"no_devices": "Эта интеграция не имеет устройств",
"no_device": "Объекты без устройств",
"delete_confirm": "Вы уверены, что хотите удалить эту интеграцию?",
"restart_confirm": "Перезапустите Home Assistant, чтобы завершить удаление этой интеграции",
"manuf": "от {manufacturer}",
"manuf": "Производитель: {manufacturer}",
"hub": "Подключено через",
"firmware": "Прошивка: {version}",
"device_unavailable": "устройство недоступно",
@ -712,6 +713,28 @@
"required_fields": "Заполните все обязательные поля"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "Отмеченные элементы",
"clear_items": "Очистить отмеченные элементы",
"add_item": "Добавить элемент"
}
},
"editor": {
"edit_card": {
"header": "Настройка карточки",
"save": "Сохранить",
"toggle_editor": "Переключить редактор"
},
"migrate": {
"header": "Конфигурация несовместима",
"para_no_id": "Этот элемент не имеет ID. Добавьте ID для этого элемента в 'ui-lovelace.yaml'.",
"para_migrate": "Home Assistant может автоматически добавить ID для всех карточек и видов, если нажать кнопку 'Перенести настройки'.",
"migrate": "Перенести настройки"
}
}
}
},
"sidebar": {

View File

@ -36,13 +36,13 @@
"armed_custom_bypass": "Vklopljen izjeme po meri"
},
"automation": {
"off": "Izklopljena",
"on": "Vklopljena"
"off": "Izkljen",
"on": "Vklopljen"
},
"binary_sensor": {
"default": {
"off": "Izklopljeno",
"on": "Vklopljeno"
"off": "Izkljen",
"on": "Vklopljen"
},
"moisture": {
"off": "Suho",
@ -57,12 +57,12 @@
"on": "Zaznano"
},
"occupancy": {
"off": "Prosto",
"on": "Zasedeno"
"off": "Čisto",
"on": "Zaznano"
},
"smoke": {
"off": "Čisto",
"on": "Zaznan"
"on": "Zaznano"
},
"sound": {
"off": "Čisto",
@ -144,7 +144,8 @@
"performance": "Učinkovitost",
"high_demand": "Visoka poraba",
"heat_pump": "Toplotna črpalka",
"gas": "Plin"
"gas": "Plin",
"manual": "Ročno"
},
"configurator": {
"configure": "Konfiguriraj",
@ -216,16 +217,16 @@
"on": "On"
},
"sensor": {
"off": "Off",
"on": "On"
"off": "Izključen",
"on": "Vklopljen"
},
"sun": {
"above_horizon": "Nad obzorjem",
"below_horizon": "Pod obzorjem"
},
"switch": {
"off": "Off",
"on": "On"
"off": "Izključen",
"on": "Vklopljen"
},
"zwave": {
"default": {
@ -281,7 +282,7 @@
"arming": "Vklapljam",
"disarming": "Izklopi",
"triggered": "Sprožen",
"armed_custom_bypass": "Vklopljen"
"armed_custom_bypass": "Aktiven"
},
"device_tracker": {
"home": "Doma",
@ -430,8 +431,8 @@
"introduction": "Pogoji so neobvezni del pravila za avtomatizacijo in jih je mogoče uporabiti za preprečitev, da bi se dejanje zgodilo ob sprožitvi. Pogoji so zelo podobni sprožilcem, vendar so zelo različni. Sprožilec bo pogledal dogodke, ki se dogajajo v sistemu, medtem ko pogoj gleda samo na to, kako sistem trenutno izgleda. Sprožilec lahko opazi, da je stikalo vklopljeno. Pogoj lahko vidi le, če je stikalo trenutno vklopljeno ali izklopljeno. \n\n [Več o pogojih.] (Https:\/\/home-assistant.io\/docs\/scripts\/conditions\/)",
"add": "Dodaj pogoj",
"duplicate": "Podvoji",
"delete": "Briši",
"delete_confirm": "Ste sigurni, da želite pobrisati?",
"delete": "Izbriši",
"delete_confirm": "Ste prepričani, da želite izbrisati?",
"unsupported_condition": "Nepodprti pogoj: {condition}",
"type_select": "Vrsta pogoja",
"type": {
@ -443,7 +444,7 @@
"label": "Numerično stanje",
"above": "Nad",
"below": "Pod",
"value_template": "Vrednostna predloga"
"value_template": "Vrednostna predloga (neobvezno)"
},
"sun": {
"label": "Sonce",
@ -465,7 +466,7 @@
},
"zone": {
"label": "Območje",
"entity": "Subjekti z lokacijo",
"entity": "Subjekt z lokacijo",
"zone": "Območje"
}
}
@ -475,8 +476,8 @@
"introduction": "Akcije so, kaj bo storil Home Assistent, ko se sproži avtomatizacija. \n\n [Več o dejavnostih.] (Https:\/\/home-assistant.io\/docs\/automation\/action\/)",
"add": "Dodaj akcijo",
"duplicate": "Podvoji",
"delete": "Briši",
"delete_confirm": "Res želite pobrisati?",
"delete": "Izbriši",
"delete_confirm": "Ste prepričani, da želite izbrisati?",
"unsupported_action": "Nepodprta akcija: {action}",
"type_select": "Vrsta ukrepa",
"type": {
@ -556,7 +557,7 @@
"profile": {
"push_notifications": {
"header": "Push obvestila",
"description": "Prikazuj obvestila na tej napravi",
"description": "Pošiljaj obvestila tej napravi",
"error_load_platform": "Konfiguriraj notify.html5 (push obvestila).",
"error_use_https": "Zahteva SSL za Frontend.",
"push_notifications": "Push obvestila",
@ -712,6 +713,28 @@
"required_fields": "Izpolnite vsa zahtevana polja"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "Označeni predmeti",
"clear_items": "Počisti označene elemente",
"add_item": "Dodaj element"
}
},
"editor": {
"edit_card": {
"header": "Konfiguracija kartice",
"save": "Shrani",
"toggle_editor": "Preklop na urejevalnik"
},
"migrate": {
"header": "Konfiguracija Nezdružljiva",
"para_no_id": "Ta element nima ID-ja. Prosimo, dodajte ID tega elementa v 'ui-lovelace.yaml'.",
"para_migrate": "Home Assistant lahko doda ID-je vsem vašim karticam in pogledom samodejno s pritiskom gumba »Migriraj nastavitve«.",
"migrate": "Migriraj nastavitve"
}
}
}
},
"sidebar": {

View File

@ -144,7 +144,8 @@
"performance": "Prestanda",
"high_demand": "Hög efterfrågan",
"heat_pump": "Värmepump",
"gas": "Gas"
"gas": "Gas",
"manual": "Manuell"
},
"configurator": {
"configure": "Konfigurera",
@ -589,7 +590,7 @@
"description": "Skapa långlivade åtkomsttokens för att låta dina skript interagera med din hemassistentenhet. Varje token kommer att vara giltig i 10 år från skapandet. Följande långlivade åtkomsttoken är för närvarande aktiva.",
"learn_auth_requests": "Lär dig hur du gör autentiserade förfrågningar.",
"created_at": "Skapades den {date}",
"confirm_delete": "Är du säker du vill radera åtkomsttoken för {namn}?",
"confirm_delete": "Är du säker du vill radera åtkomsttoken för {name}?",
"delete_failed": "Det gick inte att ta bort åtkomsttoken.",
"create": "Skapa token",
"create_failed": "Det gick inte att skapa åtkomsttoken.",
@ -712,6 +713,28 @@
"required_fields": "Fyll i alla fält som krävs"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "Markerade objekt",
"clear_items": "Rensa avbockade objekt",
"add_item": "Lägg till objekt"
}
},
"editor": {
"edit_card": {
"header": "Kortkonfiguration",
"save": "Spara",
"toggle_editor": "Visa \/ Dölj redigerare"
},
"migrate": {
"header": "Konfigurationen är inte giltig",
"para_no_id": "Det här elementet har inget ID. Lägg till ett ID till det här elementet i \"ui-lovelace.yaml\".",
"para_migrate": "Home Assistant kan automatiskt lägga till IDn till alla dina kort och vyer genom att du klickar på \"Migrera konfiguration\".",
"migrate": "Migrera konfigurationen"
}
}
}
},
"sidebar": {

View File

@ -13,6 +13,7 @@
"dev-templates": "Taslaklar",
"dev-mqtt": "MQTT",
"dev-info": "Bilgi",
"calendar": "Takvim",
"profile": "Profil"
},
"state": {
@ -139,7 +140,8 @@
"performance": "Performans",
"high_demand": "Yüksek talep",
"heat_pump": "Isı pompası",
"gas": "Gaz"
"gas": "Gaz",
"manual": "Manuel"
},
"configurator": {
"configure": "Ayarla",
@ -233,6 +235,25 @@
"initializing": "Başlatılıyor ({query_stage})",
"dead": "Ölü ({query_stage})"
}
},
"weather": {
"clear-night": "Açık, gece",
"cloudy": "Bulutlu",
"fog": "Sis",
"lightning": "Yıldırım",
"lightning-rainy": "Yıldırım, yağmurlu",
"partlycloudy": "Parçalı bulutlu",
"rainy": "Yağmurlu",
"snowy": "Karlı",
"snowy-rainy": "Karlı, yağmurlu",
"sunny": "Güneşli",
"windy": "Rüzgarlı",
"windy-variant": "Rüzgarlı"
},
"vacuum": {
"error": "Hata",
"off": "Kapalı",
"on": "Açık"
}
},
"state_badge": {
@ -322,6 +343,9 @@
"header": "Otomasyon Düzenleyici"
},
"editor": {
"save": "Kaydet",
"unsaved_confirm": "Kaydedilmemiş değişiklikleriniz var. Çıkmak istediğinize emin misiniz?",
"alias": "İsim",
"triggers": {
"delete": "Sil",
"type": {
@ -330,7 +354,9 @@
},
"zone": {
"zone": "Bölge",
"event": "Olay:"
"event": "Olay:",
"enter": "Girince",
"leave": ıkınca"
},
"state": {
"for": "İçin"
@ -354,27 +380,39 @@
"value_template": "Değer şablonu (isteğe bağlı)"
},
"sun": {
"label": "Güneş"
"label": "Güneş",
"before": "Önce:",
"after": "Sonra:",
"sunrise": "Gün Doğumu",
"sunset": "Gün Batımı"
},
"template": {
"label": "Şablon",
"value_template": "Değer şablonu"
},
"time": {
"label": "Zaman"
"label": "Zaman",
"after": "Sonra",
"before": "Önce"
},
"zone": {
"entity": "Konumlu girdi"
"label": "Bölge",
"entity": "Konumlu girdi",
"zone": "Bölge"
}
}
},
"actions": {
"duplicate": "Kopya",
"delete": "Sil",
"delete_confirm": "Silmek istediğinize emin misiniz?",
"type": {
"delay": {
"delay": "Gecikme"
},
"condition": {
"label": "Durum"
},
"event": {
"event": "Olay:",
"service_data": "Hizmet verisi"
@ -391,6 +429,18 @@
"caption": "Z-Wave",
"description": "Z-Wave ağınızı yönetin"
},
"users": {
"caption": "Kullanıcılar",
"description": "Kullanıcıları yönet",
"picker": {
"title": "Kullanıcılar"
},
"editor": {
"rename_user": "Kullanıcıyı yeniden adlandır",
"change_password": "Parolayı değiştir",
"delete_user": "Kullanıcıyı sil"
}
},
"cloud": {
"caption": "Home Assistant Cloud",
"description_login": "{email} olarak oturum açtınız",
@ -419,14 +469,22 @@
},
"profile": {
"push_notifications": {
"description": "Bu cihaza bildirimler gönder"
"description": "Bu cihaza bildirimler gönder",
"link_promo": "Daha fazla bilgi edinin"
},
"refresh_tokens": {
"description": "Her yenileme jetonu bir oturum açma oturumunu temsil eder. Yenileme jetonları, çıkış yapmak istediğinizde otomatik olarak kaldırılacaktır. Aşağıdaki yenileme jetonları hesabınız için şu anda aktif."
"description": "Her yenileme jetonu bir oturum açma oturumunu temsil eder. Yenileme jetonları, çıkış yapmak istediğinizde otomatik olarak kaldırılacaktır. Aşağıdaki yenileme jetonları hesabınız için şu anda aktif.",
"created_at": "{date} tarihinde oluşturuldu",
"last_used": "En son {date} tarihinde {location} konumundan kullanıldı",
"not_used": "Hiç kullanılmadı"
},
"long_lived_access_tokens": {
"created_at": "{date} tarihinde oluşturuldu",
"prompt_name": "Ad?",
"last_used": "En son {date} tarihinde {location} konumundan kullanıldı",
"not_used": "Hiç kullanılmamış"
},
"current_user": "{fullName} olarak giriş yaptınız.",
"logout": ıkış yap",
"change_password": {
"header": "Parolayı Değiştir",
@ -449,6 +507,77 @@
"close": "Kapat",
"submit": "Gönder"
}
},
"page-authorize": {
"form": {
"working": "Lütfen bekleyin",
"unknown_error": "Bir şeyler ters gitti",
"providers": {
"homeassistant": {
"step": {
"init": {
"data": {
"username": "Kullanıcı Adı",
"password": "Parola"
}
}
},
"error": {
"invalid_auth": "Geçersiz kullanıcı adı ya da parola"
},
"abort": {
"login_expired": "Oturum süresi doldu, lütfen tekrar giriş yapın."
}
},
"legacy_api_password": {
"step": {
"init": {
"data": {
"password": "API Parolası"
}
}
},
"error": {
"invalid_auth": "Geçersiz API parolası"
}
},
"trusted_networks": {
"step": {
"init": {
"data": {
"user": "Kullanıcı"
},
"description": "Lütfen giriş yapmak istediğiniz bir kullanıcı seçin:"
}
}
}
}
}
},
"page-onboarding": {
"user": {
"intro": "Bir kullanıcı hesabı oluşturarak başlayalım.",
"required_field": "Gerekli",
"data": {
"name": "Ad",
"username": "Kullanıcı Adı",
"password": "Parola"
},
"create_account": "Hesap Oluştur"
}
},
"lovelace": {
"cards": {
"shopping-list": {
"clear_items": "Seçili ögeleri temizle",
"add_item": "Öge Ekle"
}
},
"editor": {
"edit_card": {
"save": "Kaydet"
}
}
}
},
"sidebar": {
@ -476,6 +605,46 @@
"persistent_notification": {
"dismiss": "Kapat"
},
"weather": {
"attributes": {
"air_pressure": "Hava basıncı",
"humidity": "Nem",
"temperature": "Sıcaklık",
"visibility": "Görünürlük",
"wind_speed": "Rüzgar hızı"
}
},
"fan": {
"speed": "Hız",
"oscillate": "Salınım",
"direction": "Yön"
},
"light": {
"brightness": "Parlaklık",
"effect": "Efekt"
},
"climate": {
"on_off": "Açık \/ kapalı",
"target_temperature": "Hedef sıcaklık",
"target_humidity": "Hedef nem",
"fan_mode": "Fan modu",
"swing_mode": "Salınım modu",
"away_mode": "Dışarıda modu"
},
"lock": {
"code": "Kod",
"lock": "Kilitle",
"unlock": "Kilidi aç"
},
"media_player": {
"source": "Kaynak"
},
"vacuum": {
"actions": {
"turn_on": "Aç",
"turn_off": "Kapat"
}
},
"water_heater": {
"currently": "Güncel olarak",
"on_off": "Açık \/ kapalı",
@ -484,8 +653,20 @@
"away_mode": "Dışarıda modu"
}
},
"dialogs": {
"more_info_settings": {
"save": "Kaydet",
"name": "Ad"
}
},
"auth_store": {
"ask": "Bu girişi kaydetmek istiyor musunuz?",
"decline": "Hayır teşekkürler",
"confirm": "Girişi kaydet"
},
"notification_drawer": {
"empty": "Bildirim bulunmamakta",
"title": "Bildirimler"
}
},
"domain": {

View File

@ -73,7 +73,7 @@
"on": "Виявлено"
},
"opening": {
"off": "Закритий",
"off": "Закрито",
"on": "Відкритий"
},
"safety": {
@ -101,11 +101,11 @@
"on": "Охолодження"
},
"door": {
"off": "Зачинено",
"on": "Відкрито"
"off": "Зачинені",
"on": "Відчинені"
},
"garage_door": {
"off": "Зачинено",
"off": "ЗачиненІ",
"on": "Відкрито"
},
"heat": {
@ -114,7 +114,7 @@
},
"window": {
"off": "Зачинено",
"on": "Відкрито"
"on": "Відчинене"
},
"lock": {
"off": "Заблоковано",

View File

@ -144,7 +144,8 @@
"performance": "性能",
"high_demand": "高需求",
"heat_pump": "热泵",
"gas": "燃气"
"gas": "燃气",
"manual": "手动"
},
"configurator": {
"configure": "设置",
@ -712,6 +713,28 @@
"required_fields": "请填写所有必填字段"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "已完成项目",
"clear_items": "清除已完成项目",
"add_item": "新增项目"
}
},
"editor": {
"edit_card": {
"header": "卡片配置",
"save": "保存",
"toggle_editor": "切换编辑器"
},
"migrate": {
"header": "配置不兼容",
"para_no_id": "此元素没有 ID。请在 'ui-lovelace.yaml' 中为此元素添加 ID。",
"para_migrate": "通过点击“迁移配置”按钮Home Assistant 可以自动为您的所有卡片和视图添加 ID。",
"migrate": "迁移配置"
}
}
}
},
"sidebar": {

View File

@ -144,7 +144,8 @@
"performance": "效能",
"high_demand": "高用量",
"heat_pump": "暖氣",
"gas": "瓦斯模式"
"gas": "瓦斯模式",
"manual": "手動"
},
"configurator": {
"configure": "設定",
@ -556,7 +557,7 @@
"profile": {
"push_notifications": {
"header": "通知推送",
"description": "傳送通知推送至此裝置",
"description": "傳送通知推送至此裝置",
"error_load_platform": "設定 notify.html5。",
"error_use_https": "需要啟用前端 SSL 加密。",
"push_notifications": "通知推送",
@ -712,6 +713,28 @@
"required_fields": "填寫所有所需欄位"
}
}
},
"lovelace": {
"cards": {
"shopping-list": {
"checked_items": "已選取項目",
"clear_items": "清除已選取項目",
"add_item": "新增項目"
}
},
"editor": {
"edit_card": {
"header": "卡片設定",
"save": "儲存",
"toggle_editor": "切換編輯器"
},
"migrate": {
"header": "設定不相容",
"para_no_id": "該元件未含 ID請於「ui-lovelace.yaml」中為該元件新增 ID。",
"para_migrate": "Home Assistant 能於您點選「遷移設定」按鈕後,自動新增 ID 與視圖至所有卡片。",
"migrate": "遷移設定"
}
}
}
},
"sidebar": {

1285
yarn.lock

File diff suppressed because it is too large Load Diff