Merge pull request #3999 from home-assistant/dev

20191014.0
This commit is contained in:
Bram Kragten 2019-10-14 11:01:07 +02:00 committed by GitHub
commit f55cbd9e9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
110 changed files with 2726 additions and 1255 deletions

View File

@ -1,7 +1,6 @@
<!-- READ THIS FIRST: <!-- READ THIS FIRST:
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/ - If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases - Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
- Provide as many details as possible. Do not delete any text from this template! - Provide as many details as possible. Do not delete any text from this template!
--> -->

View File

@ -104,7 +104,10 @@ gulp.task("compress-static", () => compressStatic(paths.static));
gulp.task("copy-static-demo", (done) => { gulp.task("copy-static-demo", (done) => {
// Copy app static files // Copy app static files
fs.copySync(polyPath("public"), paths.demo_root); fs.copySync(
polyPath("public/static"),
path.resolve(paths.demo_root, "static")
);
// Copy demo static files // Copy demo static files
fs.copySync(path.resolve(paths.demo_dir, "public"), paths.demo_root); fs.copySync(path.resolve(paths.demo_dir, "public"), paths.demo_root);

20
cast/public/_headers Normal file
View File

@ -0,0 +1,20 @@
/*
Cache-Control: public, max-age: 0, s-maxage=3600, must-revalidate
Content-Security-Policy: form-action https:
Feature-Policy: vibrate 'none'; geolocation 'none'; midi 'none'; microphone 'none'; camera 'none'; magnetometer 'none'; gyroscope 'none'; speaker 'none'; vibrate 'none'; payment 'none'
Referrer-Policy: no-referrer-when-downgrade
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
/images/*
Cache-Control: public, max-age: 604800, s-maxage=604800
/manifest.json
Cache-Control: public, max-age: 3600, s-maxage=3600
/frontend_es5/*
Cache-Control: public, max-age: 604800, s-maxage=604800
/frontend_latest/*
Cache-Control: public, max-age: 604800, s-maxage=604800

18
demo/public/_headers Normal file
View File

@ -0,0 +1,18 @@
/*
Cache-Control: public, max-age: 0, s-maxage=3600, must-revalidate
Content-Security-Policy: form-action https:
Referrer-Policy: no-referrer-when-downgrade
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
/api/*
Cache-Control: public, max-age: 604800, s-maxage=604800
/assets/*
Cache-Control: public, max-age: 604800, s-maxage=604800
/frontend_es5/*
Cache-Control: public, max-age: 604800, s-maxage=604800
/frontend_latest/*
Cache-Control: public, max-age: 604800, s-maxage=604800

View File

@ -25,7 +25,7 @@
"@material/mwc-fab": "^0.8.0", "@material/mwc-fab": "^0.8.0",
"@material/mwc-ripple": "^0.8.0", "@material/mwc-ripple": "^0.8.0",
"@material/mwc-switch": "^0.8.0", "@material/mwc-switch": "^0.8.0",
"@mdi/svg": "4.4.95", "@mdi/svg": "4.5.95",
"@polymer/app-layout": "^3.0.2", "@polymer/app-layout": "^3.0.2",
"@polymer/app-localize-behavior": "^3.0.1", "@polymer/app-localize-behavior": "^3.0.1",
"@polymer/app-route": "^3.0.2", "@polymer/app-route": "^3.0.2",
@ -74,7 +74,7 @@
"@webcomponents/webcomponentsjs": "^2.2.7", "@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "~2.8.0", "chart.js": "~2.8.0",
"chartjs-chart-timeline": "^0.3.0", "chartjs-chart-timeline": "^0.3.0",
"codemirror": "^5.45.0", "codemirror": "^5.49.0",
"cpx": "^1.5.0", "cpx": "^1.5.0",
"deep-clone-simple": "^1.1.1", "deep-clone-simple": "^1.1.1",
"es6-object-assign": "^1.1.0", "es6-object-assign": "^1.1.0",
@ -117,6 +117,7 @@
"@types/chai": "^4.1.7", "@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^3.0.12", "@types/chromecast-caf-receiver": "^3.0.12",
"@types/chromecast-caf-sender": "^1.0.1", "@types/chromecast-caf-sender": "^1.0.1",
"@types/codemirror": "^0.0.78",
"@types/hls.js": "^0.12.3", "@types/hls.js": "^0.12.3",
"@types/leaflet": "^1.4.3", "@types/leaflet": "^1.4.3",
"@types/memoize-one": "4.1.0", "@types/memoize-one": "4.1.0",

View File

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

View File

@ -59,31 +59,31 @@ export interface SortingChangedEvent {
export type SortingDirection = "desc" | "asc" | null; export type SortingDirection = "desc" | "asc" | null;
export interface DataTabelColumnContainer { export interface DataTableColumnContainer {
[key: string]: DataTabelColumnData; [key: string]: DataTableColumnData;
} }
export interface DataTabelSortColumnData { export interface DataTableSortColumnData {
sortable?: boolean; sortable?: boolean;
filterable?: boolean; filterable?: boolean;
filterKey?: string; filterKey?: string;
direction?: SortingDirection; direction?: SortingDirection;
} }
export interface DataTabelColumnData extends DataTabelSortColumnData { export interface DataTableColumnData extends DataTableSortColumnData {
title: string; title: string;
type?: "numeric"; type?: "numeric" | "icon";
template?: (data: any) => TemplateResult; template?: <T>(data: any, row: T) => TemplateResult;
} }
export interface DataTabelRowData { export interface DataTableRowData {
[key: string]: any; [key: string]: any;
} }
@customElement("ha-data-table") @customElement("ha-data-table")
export class HaDataTable extends BaseElement { export class HaDataTable extends BaseElement {
@property({ type: Object }) public columns: DataTabelColumnContainer = {}; @property({ type: Object }) public columns: DataTableColumnContainer = {};
@property({ type: Array }) public data: DataTabelRowData[] = []; @property({ type: Array }) public data: DataTableRowData[] = [];
@property({ type: Boolean }) public selectable = false; @property({ type: Boolean }) public selectable = false;
@property({ type: String }) public id = "id"; @property({ type: String }) public id = "id";
protected mdcFoundation!: MDCDataTableFoundation; protected mdcFoundation!: MDCDataTableFoundation;
@ -98,9 +98,9 @@ export class HaDataTable extends BaseElement {
@property({ type: String }) private _filter = ""; @property({ type: String }) private _filter = "";
@property({ type: String }) private _sortColumn?: string; @property({ type: String }) private _sortColumn?: string;
@property({ type: String }) private _sortDirection: SortingDirection = null; @property({ type: String }) private _sortDirection: SortingDirection = null;
@property({ type: Array }) private _filteredData: DataTabelRowData[] = []; @property({ type: Array }) private _filteredData: DataTableRowData[] = [];
private _sortColumns: { private _sortColumns: {
[key: string]: DataTabelSortColumnData; [key: string]: DataTableSortColumnData;
} = {}; } = {};
private curRequest = 0; private curRequest = 0;
private _worker: any | undefined; private _worker: any | undefined;
@ -134,8 +134,8 @@ export class HaDataTable extends BaseElement {
} }
} }
const clonedColumns: DataTabelColumnContainer = deepClone(this.columns); const clonedColumns: DataTableColumnContainer = deepClone(this.columns);
Object.values(clonedColumns).forEach((column: DataTabelColumnData) => { Object.values(clonedColumns).forEach((column: DataTableColumnData) => {
delete column.title; delete column.title;
delete column.type; delete column.type;
delete column.template; delete column.template;
@ -190,9 +190,12 @@ export class HaDataTable extends BaseElement {
const [key, column] = columnEntry; const [key, column] = columnEntry;
const sorted = key === this._sortColumn; const sorted = key === this._sortColumn;
const classes = { const classes = {
"mdc-data-table__cell--numeric": Boolean( "mdc-data-table__header-cell--numeric": Boolean(
column.type && column.type === "numeric" column.type && column.type === "numeric"
), ),
"mdc-data-table__header-cell--icon": Boolean(
column.type && column.type === "icon"
),
sortable: Boolean(column.sortable), sortable: Boolean(column.sortable),
"not-sorted": Boolean(column.sortable && !sorted), "not-sorted": Boolean(column.sortable && !sorted),
}; };
@ -222,8 +225,8 @@ export class HaDataTable extends BaseElement {
<tbody class="mdc-data-table__content"> <tbody class="mdc-data-table__content">
${repeat( ${repeat(
this._filteredData!, this._filteredData!,
(row: DataTabelRowData) => row[this.id], (row: DataTableRowData) => row[this.id],
(row: DataTabelRowData) => html` (row: DataTableRowData) => html`
<tr <tr
data-row-id="${row[this.id]}" data-row-id="${row[this.id]}"
@click=${this._handleRowClick} @click=${this._handleRowClick}
@ -251,10 +254,13 @@ export class HaDataTable extends BaseElement {
"mdc-data-table__cell--numeric": Boolean( "mdc-data-table__cell--numeric": Boolean(
column.type && column.type === "numeric" column.type && column.type === "numeric"
), ),
"mdc-data-table__cell--icon": Boolean(
column.type && column.type === "icon"
),
})}" })}"
> >
${column.template ${column.template
? column.template(row[key]) ? column.template(row[key], row)
: row[key]} : row[key]}
</td> </td>
`; `;
@ -516,6 +522,11 @@ export class HaDataTable extends BaseElement {
text-align: left; text-align: left;
} }
.mdc-data-table__cell--icon {
color: var(--secondary-text-color);
text-align: center;
}
.mdc-data-table__header-cell { .mdc-data-table__header-cell {
font-family: Roboto, sans-serif; font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
@ -543,6 +554,10 @@ export class HaDataTable extends BaseElement {
text-align: left; text-align: left;
} }
.mdc-data-table__header-cell--icon {
text-align: center;
}
/* custom from here */ /* custom from here */
.mdc-data-table { .mdc-data-table {
@ -554,7 +569,7 @@ export class HaDataTable extends BaseElement {
.mdc-data-table__header-cell.sortable { .mdc-data-table__header-cell.sortable {
cursor: pointer; cursor: pointer;
} }
.mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__cell--numeric) .mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__header-cell--numeric):not(.mdc-data-table__header-cell--icon)
span { span {
position: relative; position: relative;
left: -24px; left: -24px;
@ -565,7 +580,7 @@ export class HaDataTable extends BaseElement {
.mdc-data-table__header-cell.not-sorted ha-icon { .mdc-data-table__header-cell.not-sorted ha-icon {
left: -36px; left: -36px;
} }
.mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__cell--numeric):hover .mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__header-cell--numeric):not(.mdc-data-table__header-cell--icon):hover
span { span {
left: 0px; left: 0px;
} }

View File

@ -1,7 +1,7 @@
import { import {
DataTabelColumnContainer, DataTableColumnContainer,
DataTabelColumnData, DataTableColumnData,
DataTabelRowData, DataTableRowData,
SortingDirection, SortingDirection,
} from "./ha-data-table"; } from "./ha-data-table";
@ -9,8 +9,8 @@ import memoizeOne from "memoize-one";
export const filterSortData = memoizeOne( export const filterSortData = memoizeOne(
async ( async (
data: DataTabelRowData[], data: DataTableRowData[],
columns: DataTabelColumnContainer, columns: DataTableColumnContainer,
filter: string, filter: string,
direction: SortingDirection, direction: SortingDirection,
sortColumn?: string sortColumn?: string
@ -27,8 +27,8 @@ export const filterSortData = memoizeOne(
const _memFilterData = memoizeOne( const _memFilterData = memoizeOne(
async ( async (
data: DataTabelRowData[], data: DataTableRowData[],
columns: DataTabelColumnContainer, columns: DataTableColumnContainer,
filter: string filter: string
) => { ) => {
if (!filter) { if (!filter) {
@ -40,8 +40,8 @@ const _memFilterData = memoizeOne(
const _memSortData = memoizeOne( const _memSortData = memoizeOne(
( (
data: DataTabelRowData[], data: DataTableRowData[],
columns: DataTabelColumnContainer, columns: DataTableColumnContainer,
direction: SortingDirection, direction: SortingDirection,
sortColumn: string sortColumn: string
) => { ) => {
@ -50,8 +50,8 @@ const _memSortData = memoizeOne(
); );
export const filterData = ( export const filterData = (
data: DataTabelRowData[], data: DataTableRowData[],
columns: DataTabelColumnContainer, columns: DataTableColumnContainer,
filter: string filter: string
) => ) =>
data.filter((row) => { data.filter((row) => {
@ -71,8 +71,8 @@ export const filterData = (
}); });
export const sortData = ( export const sortData = (
data: DataTabelRowData[], data: DataTableRowData[],
column: DataTabelColumnData, column: DataTableColumnData,
direction: SortingDirection, direction: SortingDirection,
sortColumn: string sortColumn: string
) => ) =>

View File

@ -0,0 +1,160 @@
import { loadCodeMirror } from "../resources/codemirror.ondemand";
import { fireEvent } from "../common/dom/fire_event";
import {
UpdatingElement,
property,
customElement,
PropertyValues,
} from "lit-element";
import { Editor } from "codemirror";
declare global {
interface HASSDomEvents {
"editor-save": undefined;
}
}
@customElement("ha-code-editor")
export class HaCodeEditor extends UpdatingElement {
public codemirror?: Editor;
@property() public mode?: string;
@property() public autofocus = false;
@property() public rtl = false;
@property() public error = false;
@property() private _value = "";
public set value(value: string) {
this._value = value;
}
public get value(): string {
return this.codemirror ? this.codemirror.getValue() : this._value;
}
public get hasComments(): boolean {
return this.shadowRoot!.querySelector("span.cm-comment") ? true : false;
}
public connectedCallback() {
super.connectedCallback();
if (!this.codemirror) {
return;
}
this.codemirror.refresh();
if (this.autofocus !== false) {
this.codemirror.focus();
}
}
protected update(changedProps: PropertyValues): void {
super.update(changedProps);
if (!this.codemirror) {
return;
}
if (changedProps.has("mode")) {
this.codemirror.setOption("mode", this.mode);
}
if (changedProps.has("autofocus")) {
this.codemirror.setOption("autofocus", this.autofocus !== false);
}
if (changedProps.has("_value") && this._value !== this.value) {
this.codemirror.setValue(this._value);
}
if (changedProps.has("rtl")) {
this.codemirror.setOption("gutters", this._calcGutters());
this._setScrollBarDirection();
}
if (changedProps.has("error")) {
this.classList.toggle("error-state", this.error);
}
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._load();
}
private async _load(): Promise<void> {
const loaded = await loadCodeMirror();
const codeMirror = loaded.codeMirror;
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot!.innerHTML = `
<style>
${loaded.codeMirrorCss}
.CodeMirror {
height: var(--code-mirror-height, auto);
direction: var(--code-mirror-direction, ltr);
}
.CodeMirror-scroll {
max-height: var(--code-mirror-max-height, --code-mirror-height);
}
.CodeMirror-gutters {
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
transition: 0.2s ease border-right;
}
:host(.error-state) .CodeMirror-gutters {
border-color: var(--error-state-color, red);
}
.CodeMirror-focused .CodeMirror-gutters {
border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));
}
.CodeMirror-linenumber {
color: var(--paper-dialog-color, var(--primary-text-color));
}
.rtl .CodeMirror-vscrollbar {
right: auto;
left: 0px;
}
.rtl-gutter {
width: 20px;
}
</style>`;
this.codemirror = codeMirror(shadowRoot, {
value: this._value,
lineNumbers: true,
tabSize: 2,
mode: this.mode,
autofocus: this.autofocus !== false,
viewportMargin: Infinity,
extraKeys: {
Tab: "indentMore",
"Shift-Tab": "indentLess",
},
gutters: this._calcGutters(),
});
this._setScrollBarDirection();
this.codemirror!.on("changes", () => this._onChange());
}
private _onChange(): void {
const newValue = this.value;
if (newValue === this._value) {
return;
}
this._value = newValue;
fireEvent(this, "value-changed", { value: this._value });
}
private _calcGutters(): string[] {
return this.rtl ? ["rtl-gutter", "CodeMirror-linenumbers"] : [];
}
private _setScrollBarDirection(): void {
if (this.codemirror) {
this.codemirror.getWrapperElement().classList.toggle("rtl", this.rtl);
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-code-editor": HaCodeEditor;
}
}

View File

@ -77,7 +77,12 @@ class LocationEditor extends LitElement {
} }
private _updateLocation(latlng: LatLng) { private _updateLocation(latlng: LatLng) {
this.location = this._ignoreFitToMap = [latlng.lat, latlng.lng]; let longitude = latlng.lng;
if (Math.abs(longitude) > 180.0) {
// Normalize longitude if map provides values beyond -180 to +180 degrees.
longitude = (((longitude % 360.0) + 540.0) % 360.0) - 180.0;
}
this.location = this._ignoreFitToMap = [latlng.lat, longitude];
fireEvent(this, "change", undefined, { bubbles: false }); fireEvent(this, "change", undefined, { bubbles: false });
} }

View File

@ -39,6 +39,15 @@ export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) =>
device_id: deviceId, device_id: deviceId,
}); });
export const fetchDeviceConditionCapabilities = (
hass: HomeAssistant,
condition: DeviceCondition
) =>
hass.callWS<DeviceCondition[]>({
type: "device_automation/condition/capabilities",
condition,
});
export const fetchDeviceTriggerCapabilities = ( export const fetchDeviceTriggerCapabilities = (
hass: HomeAssistant, hass: HomeAssistant,
trigger: DeviceTrigger trigger: DeviceTrigger

View File

@ -18,6 +18,11 @@ export interface LovelaceViewConfig {
theme?: string; theme?: string;
panel?: boolean; panel?: boolean;
background?: string; background?: string;
visible?: boolean | ShowViewConfig[];
}
export interface ShowViewConfig {
user?: string;
} }
export interface LovelaceCardConfig { export interface LovelaceCardConfig {

View File

@ -82,14 +82,16 @@ class DialogConfigEntrySystemOptions extends LitElement {
.disabled=${this._submitting} .disabled=${this._submitting}
> >
<div> <div>
${this.hass.localize( <p>
"ui.dialogs.config_entry_system_options.enable_new_entities_label" ${this.hass.localize(
)} "ui.dialogs.config_entry_system_options.enable_new_entities_label"
</div> )}
<div class="secondary"> </p>
${this.hass.localize( <p class="secondary">
"ui.dialogs.config_entry_system_options.enable_new_entities_description" ${this.hass.localize(
)} "ui.dialogs.config_entry_system_options.enable_new_entities_description"
)}
</p>
</div> </div>
</ha-switch> </ha-switch>
</div> </div>
@ -160,7 +162,9 @@ class DialogConfigEntrySystemOptions extends LitElement {
padding-bottom: 24px; padding-bottom: 24px;
color: var(--primary-text-color); color: var(--primary-text-color);
} }
p {
margin: 0;
}
.secondary { .secondary {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }

View File

@ -79,6 +79,18 @@ class StepFlowPickHandler extends LitElement {
` `
)} )}
</div> </div>
<p>
${this.hass.localize(
"ui.panel.config.integrations.note_about_integrations"
)}<br />
${this.hass.localize(
"ui.panel.config.integrations.note_about_website_reference"
)}<a href="https://www.home-assistant.io/integrations/"
>${this.hass.localize(
"ui.panel.config.integrations.home_assistant_website"
)}.</a
>
</p>
`; `;
} }
@ -119,6 +131,9 @@ class StepFlowPickHandler extends LitElement {
paper-item { paper-item {
cursor: pointer; cursor: pointer;
} }
p {
text-align: center;
}
`; `;
} }
} }

View File

@ -63,19 +63,21 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
color: var(--primary-color); color: var(--primary-color);
} }
</style> </style>
<hass-subpage header="Home Assistant Cloud"> <hass-subpage header="[[localize('ui.panel.config.cloud.caption')]]">
<div class="content"> <div class="content">
<ha-config-section is-wide="[[isWide]]"> <ha-config-section is-wide="[[isWide]]">
<span slot="header">Home Assistant Cloud</span> <span slot="header"
>[[localize('ui.panel.config.cloud.caption')]]</span
>
<div slot="introduction"> <div slot="introduction">
<p> <p>
Thank you for being part of Home Assistant Cloud. It's because [[localize('ui.panel.config.cloud.account.thank_you_note')]]
of people like you that we are able to make a great home
automation experience for everyone. Thank you!
</p> </p>
</div> </div>
<ha-card header="Nabu Casa Account"> <ha-card
header="[[localize('ui.panel.config.cloud.account.nabu_casa_account')]]"
>
<div class="account-row"> <div class="account-row">
<paper-item-body two-line=""> <paper-item-body two-line="">
[[cloudStatus.email]] [[cloudStatus.email]]
@ -86,33 +88,37 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
</div> </div>
<div class="account-row"> <div class="account-row">
<paper-item-body> Cloud connection status </paper-item-body> <paper-item-body
>[[localize('ui.panel.config.cloud.account.connection_status')]]</paper-item-body
>
<div class="status">[[cloudStatus.cloud]]</div> <div class="status">[[cloudStatus.cloud]]</div>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<a href="https://account.nabucasa.com" target="_blank" <a href="https://account.nabucasa.com" target="_blank"
><mwc-button>Manage Account</mwc-button></a ><mwc-button
>[[localize('ui.panel.config.cloud.account.manage_account')]]</mwc-button
></a
> >
<mwc-button style="float: right" on-click="handleLogout" <mwc-button style="float: right" on-click="handleLogout"
>Sign out</mwc-button >[[localize('ui.panel.config.cloud.account.sign_out')]]</mwc-button
> >
</div> </div>
</ha-card> </ha-card>
</ha-config-section> </ha-config-section>
<ha-config-section is-wide="[[isWide]]"> <ha-config-section is-wide="[[isWide]]">
<span slot="header">Integrations</span> <span slot="header"
>[[localize('ui.panel.config.cloud.account.integrations')]]</span
>
<div slot="introduction"> <div slot="introduction">
<p> <p>
Integrations for Home Assistant Cloud allow you to connect with [[localize('ui.panel.config.cloud.account.integrations_introduction')]]
services in the cloud without having to expose your Home
Assistant instance publicly on the internet.
</p> </p>
<p> <p>
Check the website for [[localize('ui.panel.config.cloud.account.integrations_introduction2')]]
<a href="https://www.nabucasa.com" target="_blank" <a href="https://www.nabucasa.com" target="_blank"
>all available features</a >[[localize('ui.panel.config.cloud.account.integrations_link_all_features')]]</a
>. >.
</p> </p>
</div> </div>
@ -160,7 +166,9 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
} }
_computeRemoteConnected(connected) { _computeRemoteConnected(connected) {
return connected ? "Connected" : "Not Connected"; return connected
? this.hass.localize("ui.panel.config.cloud.account.connected")
: this.hass.localize("ui.panel.config.cloud.account.not_connected");
} }
async _fetchSubscriptionInfo() { async _fetchSubscriptionInfo() {
@ -182,7 +190,9 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
_formatSubscription(subInfo) { _formatSubscription(subInfo) {
if (subInfo === null) { if (subInfo === null) {
return "Fetching subscription…"; return this.hass.localize(
"ui.panel.config.cloud.account.fetching_subscription"
);
} }
let description = subInfo.human_description; let description = subInfo.human_description;

View File

@ -31,7 +31,11 @@ export class CloudAlexaPref extends LitElement {
const { alexa_enabled, alexa_report_state } = this.cloudStatus!.prefs; const { alexa_enabled, alexa_report_state } = this.cloudStatus!.prefs;
return html` return html`
<ha-card header="Alexa"> <ha-card
header=${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.title"
)}
>
<div class="switch"> <div class="switch">
<ha-switch <ha-switch
.checked=${alexa_enabled} .checked=${alexa_enabled}
@ -39,15 +43,16 @@ export class CloudAlexaPref extends LitElement {
></ha-switch> ></ha-switch>
</div> </div>
<div class="card-content"> <div class="card-content">
With the Alexa integration for Home Assistant Cloud you'll be able to ${this.hass!.localize("ui.panel.config.cloud.account.alexa.info")}
control all your Home Assistant devices via any Alexa-enabled device.
<ul> <ul>
<li> <li>
<a <a
href="https://skills-store.amazon.com/deeplink/dp/B0772J1QKB?deviceType=app" href="https://skills-store.amazon.com/deeplink/dp/B0772J1QKB?deviceType=app"
target="_blank" target="_blank"
> >
Enable the Home Assistant skill for Alexa ${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.enable_ha_skill"
)}
</a> </a>
</li> </li>
<li> <li>
@ -55,37 +60,48 @@ export class CloudAlexaPref extends LitElement {
href="https://www.nabucasa.com/config/amazon_alexa/" href="https://www.nabucasa.com/config/amazon_alexa/"
target="_blank" target="_blank"
> >
Config documentation ${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.config_documentation"
)}
</a> </a>
</li> </li>
</ul> </ul>
<em
>This integration requires an Alexa-enabled device like the Amazon
Echo.</em
>
${alexa_enabled ${alexa_enabled
? html` ? html`
<h3>Enable State Reporting</h3> <div class="state-reporting">
<h3>
${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.enable_state_reporting"
)}
</h3>
<div class="state-reporting-switch">
<ha-switch
.checked=${alexa_report_state}
@change=${this._reportToggleChanged}
></ha-switch>
</div>
</div>
<p> <p>
If you enable state reporting, Home Assistant will send ${this.hass!.localize(
<b>all</b> state changes of exposed entities to Amazon. This "ui.panel.config.cloud.account.alexa.info_state_reporting"
allows you to always see the latest states in the Alexa app )}
and use the state changes to create routines.
</p> </p>
<ha-switch
.checked=${alexa_report_state}
@change=${this._reportToggleChanged}
></ha-switch>
` `
: ""} : ""}
</div> </div>
<div class="card-actions"> <div class="card-actions">
<mwc-button @click=${this._handleSync} .disabled=${this._syncing}> <mwc-button @click=${this._handleSync} .disabled=${this._syncing}>
Sync Entities ${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.sync_entities"
)}
</mwc-button> </mwc-button>
<div class="spacer"></div> <div class="spacer"></div>
<a href="/config/cloud/alexa"> <a href="/config/cloud/alexa">
<mwc-button>Manage Entities</mwc-button> <mwc-button
>${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.manage_entities"
)}</mwc-button
>
</a> </a>
</div> </div>
</ha-card> </ha-card>
@ -97,7 +113,11 @@ export class CloudAlexaPref extends LitElement {
try { try {
await syncCloudAlexaEntities(this.hass!); await syncCloudAlexaEntities(this.hass!);
} catch (err) { } catch (err) {
alert(`Failed to sync entities: ${err.body.message}`); alert(
`${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.sync_entities_error"
)} ${err.body.message}`
);
} finally { } finally {
this._syncing = false; this._syncing = false;
} }
@ -122,9 +142,15 @@ export class CloudAlexaPref extends LitElement {
fireEvent(this, "ha-refresh-cloud-status"); fireEvent(this, "ha-refresh-cloud-status");
} catch (err) { } catch (err) {
alert( alert(
`Unable to ${toggle.checked ? "enable" : "disable"} report state. ${ `${this.hass!.localize(
err.message "ui.panel.config.cloud.account.alexa.state_reporting_error",
}` "enable_disable",
this.hass!.localize(
toggle.checked
? "ui.panel.config.cloud.account.alexa.enable"
: "ui.panel.config.cloud.account.alexa.disable"
)
)} ${err.message}`
); );
toggle.checked = !toggle.checked; toggle.checked = !toggle.checked;
} }
@ -149,12 +175,22 @@ export class CloudAlexaPref extends LitElement {
.spacer { .spacer {
flex-grow: 1; flex-grow: 1;
} }
h3 { .state-reporting {
margin-bottom: 0; display: flex;
margin-top: 1.5em;
} }
h3 + p { .state-reporting + p {
margin-top: 0.5em; margin-top: 0.5em;
} }
.state-reporting h3 {
flex-grow: 1;
margin: 0;
}
.state-reporting-switch {
margin-top: 0.25em;
margin-right: 7px;
margin-left: 0.5em;
}
`; `;
} }
} }

View File

@ -43,7 +43,11 @@ export class CloudGooglePref extends LitElement {
} = this.cloudStatus.prefs; } = this.cloudStatus.prefs;
return html` return html`
<ha-card header="Google Assistant"> <ha-card
header=${this.hass!.localize(
"ui.panel.config.cloud.account.google.title"
)}
>
<div class="switch"> <div class="switch">
<ha-switch <ha-switch
id="google_enabled" id="google_enabled"
@ -52,16 +56,16 @@ export class CloudGooglePref extends LitElement {
></ha-switch> ></ha-switch>
</div> </div>
<div class="card-content"> <div class="card-content">
With the Google Assistant integration for Home Assistant Cloud you'll ${this.hass!.localize("ui.panel.config.cloud.account.google.info")}
be able to control all your Home Assistant devices via any Google
Assistant-enabled device.
<ul> <ul>
<li> <li>
<a <a
href="https://assistant.google.com/services/a/uid/00000091fd5fb875?hl=en-US" href="https://assistant.google.com/services/a/uid/00000091fd5fb875?hl=en-US"
target="_blank" target="_blank"
> >
Activate the Home Assistant skill for Google Assistant ${this.hass!.localize(
"ui.panel.config.cloud.account.google.enable_ha_skill"
)}
</a> </a>
</li> </li>
<li> <li>
@ -69,36 +73,49 @@ export class CloudGooglePref extends LitElement {
href="https://www.nabucasa.com/config/google_assistant/" href="https://www.nabucasa.com/config/google_assistant/"
target="_blank" target="_blank"
> >
Config documentation ${this.hass!.localize(
"ui.panel.config.cloud.account.google.config_documentation"
)}
</a> </a>
</li> </li>
</ul> </ul>
<em
>This integration requires a Google Assistant-enabled device like
the Google Home or Android phone.</em
>
${google_enabled ${google_enabled
? html` ? html`
<h3>Enable State Reporting</h3> <div class="state-reporting">
<h3>
${this.hass!.localize(
"ui.panel.config.cloud.account.google.enable_state_reporting"
)}
</h3>
<div class="state-reporting-switch">
<ha-switch
.checked=${google_report_state}
@change=${this._reportToggleChanged}
></ha-switch>
</div>
</div>
<p> <p>
If you enable state reporting, Home Assistant will send ${this.hass!.localize(
<b>all</b> state changes of exposed entities to Google. This "ui.panel.config.cloud.account.google.info_state_reporting"
allows you to always see the latest states in the Google app. )}
</p> </p>
<ha-switch
.checked=${google_report_state}
@change=${this._reportToggleChanged}
></ha-switch>
<div class="secure_devices"> <div class="secure_devices">
Please enter a pin to interact with security devices. Security <h3>
devices are doors, garage doors and locks. You will be asked ${this.hass!.localize(
to say/enter this pin when interacting with such devices via "ui.panel.config.cloud.account.google.security_devices"
Google Assistant. )}
</h3>
${this.hass!.localize(
"ui.panel.config.cloud.account.google.enter_pin_info"
)}
<paper-input <paper-input
label="Secure Devices Pin" label="${this.hass!.localize(
"ui.panel.config.cloud.account.google.devices_pin"
)}"
id="google_secure_devices_pin" id="google_secure_devices_pin"
placeholder="Enter a PIN to use secure devices" placeholder="${this.hass!.localize(
"ui.panel.config.cloud.account.google.enter_pin_hint"
)}"
.value=${google_secure_devices_pin || ""} .value=${google_secure_devices_pin || ""}
@change="${this._pinChanged}" @change="${this._pinChanged}"
></paper-input> ></paper-input>
@ -112,11 +129,17 @@ export class CloudGooglePref extends LitElement {
.disabled="${!google_enabled}" .disabled="${!google_enabled}"
path="cloud/google_actions/sync" path="cloud/google_actions/sync"
> >
Sync entities to Google ${this.hass!.localize(
"ui.panel.config.cloud.account.google.sync_entities"
)}
</ha-call-api-button> </ha-call-api-button>
<div class="spacer"></div> <div class="spacer"></div>
<a href="/config/cloud/google-assistant"> <a href="/config/cloud/google-assistant">
<mwc-button>Manage Entities</mwc-button> <mwc-button
>${this.hass!.localize(
"ui.panel.config.cloud.account.google.manage_entities"
)}</mwc-button
>
</a> </a>
</div> </div>
</ha-card> </ha-card>
@ -159,7 +182,11 @@ export class CloudGooglePref extends LitElement {
showSaveSuccessToast(this, this.hass!); showSaveSuccessToast(this, this.hass!);
fireEvent(this, "ha-refresh-cloud-status"); fireEvent(this, "ha-refresh-cloud-status");
} catch (err) { } catch (err) {
alert(`Unable to store pin: ${err.message}`); alert(
`${this.hass!.localize(
"ui.panel.config.cloud.account.google.enter_pin_error"
)} ${err.message}`
);
input.value = this.cloudStatus!.prefs.google_secure_devices_pin; input.value = this.cloudStatus!.prefs.google_secure_devices_pin;
} }
} }
@ -179,7 +206,7 @@ export class CloudGooglePref extends LitElement {
font-weight: 500; font-weight: 500;
} }
.secure_devices { .secure_devices {
padding-top: 16px; padding-top: 8px;
} }
paper-input { paper-input {
width: 250px; width: 250px;
@ -193,6 +220,25 @@ export class CloudGooglePref extends LitElement {
.spacer { .spacer {
flex-grow: 1; flex-grow: 1;
} }
.state-reporting {
display: flex;
margin-top: 1.5em;
}
.state-reporting + p {
margin-top: 0.5em;
}
h3 {
margin: 0 0 8px 0;
}
.state-reporting h3 {
flex-grow: 1;
margin: 0;
}
.state-reporting-switch {
margin-top: 0.25em;
margin-right: 7px;
margin-left: 0.5em;
}
`; `;
} }
} }

View File

@ -49,16 +49,26 @@ export class CloudRemotePref extends LitElement {
if (!remote_certificate) { if (!remote_certificate) {
return html` return html`
<ha-card header="Remote Control"> <ha-card
header=${this.hass!.localize(
"ui.panel.config.cloud.account.remote.title"
)}
>
<div class="preparing"> <div class="preparing">
Remote access is being prepared. We will notify you when it's ready. ${this.hass!.localize(
"ui.panel.config.cloud.account.remote.access_is_being_prepared"
)}
</div> </div>
</ha-card> </ha-card>
`; `;
} }
return html` return html`
<ha-card header="Remote Control"> <ha-card
header=${this.hass!.localize(
"ui.panel.config.cloud.account.remote.title"
)}
>
<div class="switch"> <div class="switch">
<ha-switch <ha-switch
.checked="${remote_connected}" .checked="${remote_connected}"
@ -66,22 +76,33 @@ export class CloudRemotePref extends LitElement {
></ha-switch> ></ha-switch>
</div> </div>
<div class="card-content"> <div class="card-content">
Home Assistant Cloud provides a secure remote connection to your ${this.hass!.localize("ui.panel.config.cloud.account.remote.info")}
instance while away from home. Your instance ${remote_connected
${remote_connected ? "is" : "will be"} available at ? this.hass!.localize(
<a href="https://${remote_domain}" target="_blank"> "ui.panel.config.cloud.account.remote.instance_is_available"
)
: this.hass!.localize(
"ui.panel.config.cloud.account.remote.instance_will_be_available"
)}
<a href="https://${remote_domain}" target="_blank" class="break-word">
https://${remote_domain}</a https://${remote_domain}</a
>. >.
</div> </div>
<div class="card-actions"> <div class="card-actions">
<a href="https://www.nabucasa.com/config/remote/" target="_blank"> <a href="https://www.nabucasa.com/config/remote/" target="_blank">
<mwc-button>Learn how it works</mwc-button> <mwc-button
>${this.hass!.localize(
"ui.panel.config.cloud.account.remote.link_learn_how_it_works"
)}</mwc-button
>
</a> </a>
${remote_certificate ${remote_certificate
? html` ? html`
<div class="spacer"></div> <div class="spacer"></div>
<mwc-button @click=${this._openCertInfo}> <mwc-button @click=${this._openCertInfo}>
Certificate Info ${this.hass!.localize(
"ui.panel.config.cloud.account.remote.certificate_info"
)}
</mwc-button> </mwc-button>
` `
: ""} : ""}
@ -120,6 +141,9 @@ export class CloudRemotePref extends LitElement {
a { a {
color: var(--primary-color); color: var(--primary-color);
} }
.break-word {
overflow-wrap: break-word;
}
.switch { .switch {
position: absolute; position: absolute;
right: 24px; right: 24px;

View File

@ -51,16 +51,20 @@ export class CloudWebhooks extends LitElement {
protected render() { protected render() {
return html` return html`
${this.renderStyle()} ${this.renderStyle()}
<ha-card header="Webhooks"> <ha-card
header=${this.hass!.localize(
"ui.panel.config.cloud.account.webhooks.title"
)}
>
<div class="card-content"> <div class="card-content">
Anything that is configured to be triggered by a webhook can be given ${this.hass!.localize("ui.panel.config.cloud.account.webhooks.info")}
a publicly accessible URL to allow you to send data back to Home ${this._renderBody()}
Assistant from anywhere, without exposing your instance to the
internet. ${this._renderBody()}
<div class="footer"> <div class="footer">
<a href="https://www.nabucasa.com/config/webhooks" target="_blank"> <a href="https://www.nabucasa.com/config/webhooks" target="_blank">
Learn more about creating webhook-powered automations. ${this.hass!.localize(
"ui.panel.config.cloud.account.webhooks.link_learn_more"
)}
</a> </a>
</div> </div>
</div> </div>
@ -78,16 +82,33 @@ export class CloudWebhooks extends LitElement {
private _renderBody() { private _renderBody() {
if (!this.cloudStatus || !this._localHooks || !this._cloudHooks) { if (!this.cloudStatus || !this._localHooks || !this._cloudHooks) {
return html` return html`
<div class="body-text">Loading</div> <div class="body-text">
${this.hass!.localize(
"ui.panel.config.cloud.account.webhooks.loading"
)}
</div>
`; `;
} }
if (this._localHooks.length === 0) { if (this._localHooks.length === 0) {
return html` return html`
<div class="body-text"> <div class="body-text">
Looks like you have no webhooks yet. Get started by configuring a ${this.hass!.localize(
<a href="/config/integrations">webhook-based integration</a> or by "ui.panel.config.cloud.account.webhooks.no_hooks_yet"
creating a <a href="/config/automation/new">webhook automation</a>. )}
<a href="/config/integrations"
>${this.hass!.localize(
"ui.panel.config.cloud.account.webhooks.no_hooks_yet_link_integration"
)}</a
>
${this.hass!.localize(
"ui.panel.config.cloud.account.webhooks.no_hooks_yet2"
)}
<a href="/config/automation/new"
>${this.hass!.localize(
"ui.panel.config.cloud.account.webhooks.no_hooks_yet_link_automation"
)}</a
>.
</div> </div>
`; `;
} }
@ -113,7 +134,9 @@ export class CloudWebhooks extends LitElement {
: this._cloudHooks![entry.webhook_id] : this._cloudHooks![entry.webhook_id]
? html` ? html`
<mwc-button @click="${this._handleManageButton}"> <mwc-button @click="${this._handleManageButton}">
Manage ${this.hass!.localize(
"ui.panel.config.cloud.account.webhooks.manage"
)}
</mwc-button> </mwc-button>
` `
: html` : html`
@ -171,7 +194,11 @@ export class CloudWebhooks extends LitElement {
try { try {
await deleteCloudhook(this.hass!, webhookId!); await deleteCloudhook(this.hass!, webhookId!);
} catch (err) { } catch (err) {
alert(`Failed to disable webhook: ${(err as WebhookError).message}`); alert(
`${this.hass!.localize(
"ui.panel.config.cloud.account.webhooks.disable_hook_error_msg"
)} ${(err as WebhookError).message}`
);
return; return;
} finally { } finally {
this._progress = this._progress.filter((wid) => wid !== webhookId); this._progress = this._progress.filter((wid) => wid !== webhookId);

View File

@ -136,7 +136,7 @@ class CloudAlexa extends LitElement {
.checked=${isExposed} .checked=${isExposed}
@change=${this._exposeChanged} @change=${this._exposeChanged}
> >
Expose to Alexa ${this.hass!.localize("ui.panel.config.cloud.alexa.expose")}
</ha-switch> </ha-switch>
</div> </div>
</ha-card> </ha-card>
@ -148,7 +148,9 @@ class CloudAlexa extends LitElement {
} }
return html` return html`
<hass-subpage header="Alexa"> <hass-subpage header="${this.hass!.localize(
"ui.panel.config.cloud.alexa.title"
)}">
<span slot="toolbar-icon"> <span slot="toolbar-icon">
${selected}${ ${selected}${
!this.narrow !this.narrow
@ -173,9 +175,7 @@ class CloudAlexa extends LitElement {
!emptyFilter !emptyFilter
? html` ? html`
<div class="banner"> <div class="banner">
Editing which entities are exposed via this UI is disabled ${this.hass!.localize("ui.panel.config.cloud.alexa.banner")}
because you have configured entity filters in
configuration.yaml.
</div> </div>
` `
: "" : ""
@ -183,7 +183,11 @@ class CloudAlexa extends LitElement {
${ ${
exposedCards.length > 0 exposedCards.length > 0
? html` ? html`
<h1>Exposed entities</h1> <h1>
${this.hass!.localize(
"ui.panel.config.cloud.alexa.exposed_entities"
)}
</h1>
<div class="content">${exposedCards}</div> <div class="content">${exposedCards}</div>
` `
: "" : ""
@ -191,7 +195,11 @@ class CloudAlexa extends LitElement {
${ ${
notExposedCards.length > 0 notExposedCards.length > 0
? html` ? html`
<h1>Not Exposed entities</h1> <h1>
${this.hass!.localize(
"ui.panel.config.cloud.alexa.not_exposed_entities"
)}
</h1>
<div class="content">${notExposedCards}</div> <div class="content">${notExposedCards}</div>
` `
: "" : ""

View File

@ -40,23 +40,38 @@ class DialogCloudCertificate extends LitElement {
return html` return html`
<ha-paper-dialog with-backdrop> <ha-paper-dialog with-backdrop>
<h2>Certificate Information</h2> <h2>
${this.hass!.localize(
"ui.panel.config.cloud.dialog_certificate.certificate_information"
)}
</h2>
<div> <div>
<p> <p>
Certificate expiration date: ${this.hass!.localize(
"ui.panel.config.cloud.dialog_certificate.certificate_expiration_date"
)}
${format_date_time( ${format_date_time(
new Date(certificateInfo.expire_date), new Date(certificateInfo.expire_date),
this.hass!.language this.hass!.language
)}<br /> )}<br />
(Will be automatically renewed) (${this.hass!.localize(
"ui.panel.config.cloud.dialog_certificate.will_be_auto_renewed"
)})
</p> </p>
<p> <p class="break-word">
Certificate fingerprint: ${certificateInfo.fingerprint} ${this.hass!.localize(
"ui.panel.config.cloud.dialog_certificate.fingerprint"
)}
${certificateInfo.fingerprint}
</p> </p>
</div> </div>
<div class="paper-dialog-buttons"> <div class="paper-dialog-buttons">
<mwc-button @click="${this._closeDialog}">CLOSE</mwc-button> <mwc-button @click="${this._closeDialog}"
>${this.hass!.localize(
"ui.panel.config.cloud.dialog_certificate.close"
)}</mwc-button
>
</div> </div>
</ha-paper-dialog> </ha-paper-dialog>
`; `;
@ -77,6 +92,9 @@ class DialogCloudCertificate extends LitElement {
ha-paper-dialog { ha-paper-dialog {
width: 535px; width: 535px;
} }
.break-word {
overflow-wrap: break-word;
}
`, `,
]; ];
} }

View File

@ -50,9 +50,19 @@ export class DialogManageCloudhook extends LitElement {
: `https://www.home-assistant.io/integrations/${webhook.domain}/`; : `https://www.home-assistant.io/integrations/${webhook.domain}/`;
return html` return html`
<ha-paper-dialog with-backdrop> <ha-paper-dialog with-backdrop>
<h2>Webhook for ${webhook.name}</h2> <h2>
${this.hass!.localize(
"ui.panel.config.cloud.dialog_cloudhook.webhook_for",
"name",
webhook.name
)}
</h2>
<div> <div>
<p>The webhook is available at the following url:</p> <p>
${this.hass!.localize(
"ui.panel.config.cloud.dialog_cloudhook.available_at"
)}
</p>
<paper-input <paper-input
label="${inputLabel}" label="${inputLabel}"
value="${cloudhook.cloudhook_url}" value="${cloudhook.cloudhook_url}"
@ -62,13 +72,18 @@ export class DialogManageCloudhook extends LitElement {
<p> <p>
${cloudhook.managed ${cloudhook.managed
? html` ? html`
This webhook is managed by an integration and cannot be ${this.hass!.localize(
disabled. "ui.panel.config.cloud.dialog_cloudhook.managed_by_integration"
)}
` `
: html` : html`
If you no longer want to use this webhook, you can ${this.hass!.localize(
"ui.panel.config.cloud.dialog_cloudhook.info_disable_webhook"
)}
<button class="link" @click="${this._disableWebhook}"> <button class="link" @click="${this._disableWebhook}">
disable it</button ${this.hass!.localize(
"ui.panel.config.cloud.dialog_cloudhook.link_disable_webhook"
)}</button
>. >.
`} `}
</p> </p>
@ -76,9 +91,17 @@ export class DialogManageCloudhook extends LitElement {
<div class="paper-dialog-buttons"> <div class="paper-dialog-buttons">
<a href="${docsUrl}" target="_blank"> <a href="${docsUrl}" target="_blank">
<mwc-button>VIEW DOCUMENTATION</mwc-button> <mwc-button
>${this.hass!.localize(
"ui.panel.config.cloud.dialog_cloudhook.view_documentation"
)}</mwc-button
>
</a> </a>
<mwc-button @click="${this._closeDialog}">CLOSE</mwc-button> <mwc-button @click="${this._closeDialog}"
>${this.hass!.localize(
"ui.panel.config.cloud.dialog_cloudhook.close"
)}</mwc-button
>
</div> </div>
</ha-paper-dialog> </ha-paper-dialog>
`; `;
@ -97,7 +120,13 @@ export class DialogManageCloudhook extends LitElement {
} }
private async _disableWebhook() { private async _disableWebhook() {
if (!confirm("Are you sure you want to disable this webhook?")) { if (
!confirm(
this.hass!.localize(
"ui.panel.config.cloud.dialog_cloudhook.confirm_disable"
)
)
) {
return; return;
} }
@ -113,7 +142,9 @@ export class DialogManageCloudhook extends LitElement {
input.setSelectionRange(0, input.value.length); input.setSelectionRange(0, input.value.length);
try { try {
document.execCommand("copy"); document.execCommand("copy");
paperInput.label = "COPIED TO CLIPBOARD"; paperInput.label = this.hass!.localize(
"ui.panel.config.cloud.dialog_cloudhook.copied_to_clipboard"
);
} catch (err) { } catch (err) {
// Copying failed. Oh no // Copying failed. Oh no
} }

View File

@ -7,11 +7,12 @@ import "../../../../components/buttons/ha-progress-button";
import "../../../../layouts/hass-subpage"; import "../../../../layouts/hass-subpage";
import "../../../../resources/ha-style"; import "../../../../resources/ha-style";
import { EventsMixin } from "../../../../mixins/events-mixin"; import { EventsMixin } from "../../../../mixins/events-mixin";
import LocalizeMixin from "../../../../mixins/localize-mixin";
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
*/ */
class CloudForgotPassword extends EventsMixin(PolymerElement) { class CloudForgotPassword extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() { static get template() {
return html` return html`
<style include="iron-flex ha-style"> <style include="iron-flex ha-style">
@ -44,30 +45,29 @@ class CloudForgotPassword extends EventsMixin(PolymerElement) {
display: none; display: none;
} }
</style> </style>
<hass-subpage header="Forgot Password"> <hass-subpage header=[[localize('ui.panel.config.cloud.forgot_password.title')]]>
<div class="content"> <div class="content">
<ha-card header="Forgot your password"> <ha-card header=[[localize('ui.panel.config.cloud.forgot_password.subtitle')]]>
<div class="card-content"> <div class="card-content">
<p> <p>
Enter your email address and we will send you a link to reset [[localize('ui.panel.config.cloud.forgot_password.instructions')]]
your password.
</p> </p>
<div class="error" hidden$="[[!_error]]">[[_error]]</div> <div class="error" hidden$="[[!_error]]">[[_error]]</div>
<paper-input <paper-input
autofocus="" autofocus=""
id="email" id="email"
label="E-mail" label="[[localize('ui.panel.config.cloud.forgot_password.email')]]"
value="{{email}}" value="{{email}}"
type="email" type="email"
on-keydown="_keyDown" on-keydown="_keyDown"
error-message="Invalid email" error-message="[[localize('ui.panel.config.cloud.forgot_password.email_error_msg')]]"
></paper-input> ></paper-input>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-progress-button <ha-progress-button
on-click="_handleEmailPasswordReset" on-click="_handleEmailPasswordReset"
progress="[[_requestInProgress]]" progress="[[_requestInProgress]]"
>Send reset email</ha-progress-button >[[localize('ui.panel.config.cloud.forgot_password.send_reset_email')]]</ha-progress-button
> >
</div> </div>
</ha-card> </ha-card>
@ -126,7 +126,7 @@ class CloudForgotPassword extends EventsMixin(PolymerElement) {
this._requestInProgress = false; this._requestInProgress = false;
this.fire("cloud-done", { this.fire("cloud-done", {
flashMessage: flashMessage:
"Check your email for instructions on how to reset your password.", "[[localize('ui.panel.config.cloud.forgot_password.check_your_email')]]",
}); });
}, },
(err) => (err) =>

View File

@ -132,7 +132,7 @@ class CloudGoogleAssistant extends LitElement {
.checked=${isExposed} .checked=${isExposed}
@change=${this._exposeChanged} @change=${this._exposeChanged}
> >
Expose to Google Assistant ${this.hass!.localize("ui.panel.config.cloud.google.expose")}
</ha-switch> </ha-switch>
${entity.might_2fa ${entity.might_2fa
? html` ? html`
@ -141,7 +141,9 @@ class CloudGoogleAssistant extends LitElement {
.checked=${Boolean(config.disable_2fa)} .checked=${Boolean(config.disable_2fa)}
@change=${this._disable2FAChanged} @change=${this._disable2FAChanged}
> >
Disable two factor authentication ${this.hass!.localize(
"ui.panel.config.cloud.google.disable_2FA"
)}
</ha-switch> </ha-switch>
` `
: ""} : ""}
@ -155,7 +157,9 @@ class CloudGoogleAssistant extends LitElement {
} }
return html` return html`
<hass-subpage header="Google Assistant"> <hass-subpage header="${this.hass!.localize(
"ui.panel.config.cloud.google.title"
)}">
<span slot="toolbar-icon"> <span slot="toolbar-icon">
${selected}${ ${selected}${
!this.narrow !this.narrow
@ -180,9 +184,7 @@ class CloudGoogleAssistant extends LitElement {
!emptyFilter !emptyFilter
? html` ? html`
<div class="banner"> <div class="banner">
Editing which entities are exposed via this UI is disabled ${this.hass!.localize("ui.panel.config.cloud.google.banner")}
because you have configured entity filters in
configuration.yaml.
</div> </div>
` `
: "" : ""
@ -190,7 +192,11 @@ class CloudGoogleAssistant extends LitElement {
${ ${
exposedCards.length > 0 exposedCards.length > 0
? html` ? html`
<h1>Exposed entities</h1> <h1>
${this.hass!.localize(
"ui.panel.config.cloud.google.exposed_entities"
)}
</h1>
<div class="content">${exposedCards}</div> <div class="content">${exposedCards}</div>
` `
: "" : ""
@ -198,7 +204,11 @@ class CloudGoogleAssistant extends LitElement {
${ ${
notExposedCards.length > 0 notExposedCards.length > 0
? html` ? html`
<h1>Not Exposed entities</h1> <h1>
${this.hass!.localize(
"ui.panel.config.cloud.google.not_exposed_entities"
)}
</h1>
<div class="content">${notExposedCards}</div> <div class="content">${notExposedCards}</div>
` `
: "" : ""
@ -323,7 +333,11 @@ class CloudGoogleAssistant extends LitElement {
window.addEventListener( window.addEventListener(
"popstate", "popstate",
() => { () => {
showToast(parent, { message: "Synchronizing changes to Google." }); showToast(parent, {
message: this.hass!.localize(
"ui.panel.config.cloud.googe.sync_to_google"
),
});
cloudSyncGoogleAssistant(this.hass); cloudSyncGoogleAssistant(this.hass);
}, },
{ once: true } { once: true }

View File

@ -16,11 +16,15 @@ import "../../ha-config-section";
import { EventsMixin } from "../../../../mixins/events-mixin"; import { EventsMixin } from "../../../../mixins/events-mixin";
import NavigateMixin from "../../../../mixins/navigate-mixin"; import NavigateMixin from "../../../../mixins/navigate-mixin";
import "../../../../components/ha-icon-next"; import "../../../../components/ha-icon-next";
import LocalizeMixin from "../../../../mixins/localize-mixin";
/* /*
* @appliesMixin NavigateMixin * @appliesMixin NavigateMixin
* @appliesMixin EventsMixin * @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
*/ */
class CloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) { class CloudLogin extends LocalizeMixin(
NavigateMixin(EventsMixin(PolymerElement))
) {
static get template() { static get template() {
return html` return html`
<style include="iron-flex ha-style"> <style include="iron-flex ha-style">
@ -68,31 +72,29 @@ class CloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
</style> </style>
<hass-subpage header="Cloud Login"> <hass-subpage header="[[localize('ui.panel.config.cloud.caption')]]">
<div class="content"> <div class="content">
<ha-config-section is-wide="[[isWide]]"> <ha-config-section is-wide="[[isWide]]">
<span slot="header">Home Assistant Cloud</span> <span slot="header"
>[[localize('ui.panel.config.cloud.caption')]]</span
>
<div slot="introduction"> <div slot="introduction">
<p> <p>
Home Assistant Cloud provides you with a secure remote [[localize('ui.panel.config.cloud.login.introduction')]]
connection to your instance while away from home. It also allows
you to connect with cloud-only services: Amazon Alexa and Google
Assistant.
</p> </p>
<p> <p>
This service is run by our partner [[localize('ui.panel.config.cloud.login.introduction2')]]
<a href="https://www.nabucasa.com" target="_blank" <a href="https://www.nabucasa.com" target="_blank"
>Nabu&nbsp;Casa,&nbsp;Inc</a >Nabu&nbsp;Casa,&nbsp;Inc</a
>, a company founded by the founders of Home Assistant and >
Hass.io. [[localize('ui.panel.config.cloud.login.introduction2a')]]
</p> </p>
<p> <p>
Home Assistant Cloud is a subscription service with a free one [[localize('ui.panel.config.cloud.login.introduction3')]]
month trial. No payment information necessary.
</p> </p>
<p> <p>
<a href="https://www.nabucasa.com" target="_blank" <a href="https://www.nabucasa.com" target="_blank"
>Learn more about Home Assistant Cloud</a >[[localize('ui.panel.config.cloud.login.learn_more_link')]]</a
> >
</p> </p>
</div> </div>
@ -101,44 +103,46 @@ class CloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) {
<div class="card-content flash-msg"> <div class="card-content flash-msg">
[[flashMessage]] [[flashMessage]]
<paper-icon-button icon="hass:close" on-click="_dismissFlash" <paper-icon-button icon="hass:close" on-click="_dismissFlash"
>Dismiss</paper-icon-button >[[localize('ui.panel.config.cloud.login.dismiss')]]</paper-icon-button
> >
<paper-ripple id="flashRipple" noink=""></paper-ripple> <paper-ripple id="flashRipple" noink=""></paper-ripple>
</div> </div>
</ha-card> </ha-card>
<ha-card header="Sign in"> <ha-card
header="[[localize('ui.panel.config.cloud.login.sign_in')]]"
>
<div class="card-content"> <div class="card-content">
<div class="error" hidden$="[[!_error]]">[[_error]]</div> <div class="error" hidden$="[[!_error]]">[[_error]]</div>
<paper-input <paper-input
label="Email" label="[[localize('ui.panel.config.cloud.login.email')]]"
id="email" id="email"
type="email" type="email"
value="{{email}}" value="{{email}}"
on-keydown="_keyDown" on-keydown="_keyDown"
error-message="Invalid email" error-message="[[localize('ui.panel.config.cloud.login.email_error_msg')]]"
></paper-input> ></paper-input>
<paper-input <paper-input
id="password" id="password"
label="Password" label="[[localize('ui.panel.config.cloud.login.password')]]"
value="{{_password}}" value="{{_password}}"
type="password" type="password"
on-keydown="_keyDown" on-keydown="_keyDown"
error-message="Passwords are at least 8 characters" error-message="[[localize('ui.panel.config.cloud.login.password_error_msg')]]"
></paper-input> ></paper-input>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-progress-button <ha-progress-button
on-click="_handleLogin" on-click="_handleLogin"
progress="[[_requestInProgress]]" progress="[[_requestInProgress]]"
>Sign in</ha-progress-button >[[localize('ui.panel.config.cloud.login.sign_in')]]</ha-progress-button
> >
<button <button
class="link" class="link"
hidden="[[_requestInProgress]]" hidden="[[_requestInProgress]]"
on-click="_handleForgotPassword" on-click="_handleForgotPassword"
> >
forgot password? [[localize('ui.panel.config.cloud.login.forgot_password')]]
</button> </button>
</div> </div>
</ha-card> </ha-card>
@ -146,8 +150,10 @@ class CloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) {
<ha-card> <ha-card>
<paper-item on-click="_handleRegister"> <paper-item on-click="_handleRegister">
<paper-item-body two-line=""> <paper-item-body two-line="">
Start your free 1 month trial [[localize('ui.panel.config.cloud.login.start_trial')]]
<div secondary="">No payment information necessary</div> <div secondary="">
[[localize('ui.panel.config.cloud.login.trial_info')]]
</div>
</paper-item-body> </paper-item-body>
<ha-icon-next></ha-icon-next> <ha-icon-next></ha-icon-next>
</paper-item> </paper-item>
@ -251,7 +257,9 @@ class CloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) {
const errCode = err && err.body && err.body.code; const errCode = err && err.body && err.body.code;
if (errCode === "PasswordChangeRequired") { if (errCode === "PasswordChangeRequired") {
alert("You need to change your password before logging in."); alert(
"[[localize('ui.panel.config.cloud.login.alert_password_change_required')]]"
);
this.navigate("/config/cloud/forgot-password"); this.navigate("/config/cloud/forgot-password");
return; return;
} }
@ -265,7 +273,8 @@ class CloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) {
}; };
if (errCode === "UserNotConfirmed") { if (errCode === "UserNotConfirmed") {
props._error = "You need to confirm your email before logging in."; props._error =
"[[localize('ui.panel.config.cloud.login.alert_email_confirm_necessary')]]";
} }
this.setProperties(props); this.setProperties(props);

View File

@ -8,11 +8,13 @@ import "../../../../layouts/hass-subpage";
import "../../../../resources/ha-style"; import "../../../../resources/ha-style";
import "../../ha-config-section"; import "../../ha-config-section";
import { EventsMixin } from "../../../../mixins/events-mixin"; import { EventsMixin } from "../../../../mixins/events-mixin";
import LocalizeMixin from "../../../../mixins/localize-mixin";
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
*/ */
class CloudRegister extends EventsMixin(PolymerElement) { class CloudRegister extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() { static get template() {
return html` return html`
<style include="iron-flex ha-style"> <style include="iron-flex ha-style">
@ -48,47 +50,47 @@ class CloudRegister extends EventsMixin(PolymerElement) {
display: none; display: none;
} }
</style> </style>
<hass-subpage header="Register Account"> <hass-subpage header="[[localize('ui.panel.config.cloud.register.title')]]">
<div class="content"> <div class="content">
<ha-config-section is-wide="[[isWide]]"> <ha-config-section is-wide="[[isWide]]">
<span slot="header">Start your free trial</span> <span slot="header">[[localize('ui.panel.config.cloud.register.headline')]]</span>
<div slot="introduction"> <div slot="introduction">
<p> <p>
Create an account to start your free one month trial with Home Assistant Cloud. No payment information necessary. [[localize('ui.panel.config.cloud.register.information')]]
</p> </p>
<p> <p>
The trial will give you access to all the benefits of Home Assistant Cloud, including: [[localize('ui.panel.config.cloud.register.information2')]]
</p> </p>
<ul> <ul>
<li>Control of Home Assistant away from home</li> <li>[[localize('ui.panel.config.cloud.register.feature_remote_control')]]</li>
<li>Integration with Google Assistant</li> <li>[[localize('ui.panel.config.cloud.register.feature_google_home')]]</li>
<li>Integration with Amazon Alexa</li> <li>[[localize('ui.panel.config.cloud.register.feature_amazon_alexa')]]</li>
<li>Easy integration with webhook-based apps like OwnTracks</li> <li>[[localize('ui.panel.config.cloud.register.feature_webhook_apps')]]</li>
</ul> </ul>
<p> <p>
This service is run by our partner <a href='https://www.nabucasa.com' target='_blank'>Nabu&nbsp;Casa,&nbsp;Inc</a>, a company founded by the founders of Home Assistant and Hass.io. [[localize('ui.panel.config.cloud.register.information3')]] <a href='https://www.nabucasa.com' target='_blank'>Nabu&nbsp;Casa,&nbsp;Inc</a>[[localize('ui.panel.config.cloud.register.information3a')]]
</p> </p>
<p> <p>
By registering an account you agree to the following terms and conditions. [[localize('ui.panel.config.cloud.register.information4')]]
</p><ul> </p><ul>
<li><a href="https://home-assistant.io/tos/" target="_blank">Terms and Conditions</a></li> <li><a href="https://home-assistant.io/tos/" target="_blank">[[localize('ui.panel.config.cloud.register.link_terms_conditions')]]</a></li>
<li><a href="https://home-assistant.io/privacy/" target="_blank">Privacy Policy</a></li> <li><a href="https://home-assistant.io/privacy/" target="_blank">[[localize('ui.panel.config.cloud.register.link_privacy_policy')]]</a></li>
</ul> </ul>
</p> </p>
</div> </div>
<ha-card header="Create Account"> <ha-card header="[[localize('ui.panel.config.cloud.register.create_account')]]">
<div class="card-content"> <div class="card-content">
<div class="header"> <div class="header">
<div class="error" hidden$="[[!_error]]">[[_error]]</div> <div class="error" hidden$="[[!_error]]">[[_error]]</div>
</div> </div>
<paper-input autofocus="" id="email" label="Email address" type="email" value="{{email}}" on-keydown="_keyDown" error-message="Invalid email"></paper-input> <paper-input autofocus="" id="email" label="[[localize('ui.panel.config.cloud.register.email_address')]]" type="email" value="{{email}}" on-keydown="_keyDown" error-message="[[localize('ui.panel.config.cloud.register.email_error_msg')]]"></paper-input>
<paper-input id="password" label="Password" value="{{_password}}" type="password" on-keydown="_keyDown" error-message="Your password needs to be at least 8 characters"></paper-input> <paper-input id="password" label="Password" value="{{_password}}" type="password" on-keydown="_keyDown" error-message="[[localize('ui.panel.config.cloud.register.password_error_msg')]]"></paper-input>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-progress-button on-click="_handleRegister" progress="[[_requestInProgress]]">Start trial</ha-progress-button> <ha-progress-button on-click="_handleRegister" progress="[[_requestInProgress]]">[[localize('ui.panel.config.cloud.register.start_trial')]]</ha-progress-button>
<button class="link" hidden="[[_requestInProgress]]" on-click="_handleResendVerifyEmail">Resend confirmation email</button> <button class="link" hidden="[[_requestInProgress]]" on-click="_handleResendVerifyEmail">[[localize('ui.panel.config.cloud.register.resend_confirmation_email')]]</button>
</div> </div>
</ha-card> </ha-card>
</ha-config-section> </ha-config-section>
@ -211,8 +213,9 @@ class CloudRegister extends EventsMixin(PolymerElement) {
_password: "", _password: "",
}); });
this.fire("cloud-done", { this.fire("cloud-done", {
flashMessage: flashMessage: this.hass.localize(
"Account created! Check your email for instructions on how to activate your account.", "ui.panel.config.cloud.register.account_created"
),
}); });
} }
} }

View File

@ -1,35 +1,17 @@
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../layouts/hass-subpage";
import { EventsMixin } from "../../../../mixins/events-mixin"; import { EventsMixin } from "../../../../mixins/events-mixin";
import LocalizeMixin from "../../../../mixins/localize-mixin"; import LocalizeMixin from "../../../../mixins/localize-mixin";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import "../../../../components/entity/state-badge";
import { compare } from "../../../../common/string/compare"; import { compare } from "../../../../common/string/compare";
import { import { updateDeviceRegistryEntry } from "../../../../data/device_registry";
subscribeDeviceRegistry,
updateDeviceRegistryEntry,
} from "../../../../data/device_registry";
import { subscribeAreaRegistry } from "../../../../data/area_registry";
import { import {
loadDeviceRegistryDetailDialog, loadDeviceRegistryDetailDialog,
showDeviceRegistryDetailDialog, showDeviceRegistryDetailDialog,
} from "../../../../dialogs/device-registry-detail/show-dialog-device-registry-detail"; } from "../../../../dialogs/device-registry-detail/show-dialog-device-registry-detail";
function computeEntityName(hass, entity) {
if (entity.name) return entity.name;
const state = hass.states[entity.entity_id];
return state ? computeStateName(state) : null;
}
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin
*/ */
@ -37,10 +19,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
static get template() { static get template() {
return html` return html`
<style> <style>
:host(:not([narrow])) .device-entities {
max-height: 225px;
overflow: auto;
}
ha-card { ha-card {
flex: 1 0 100%; flex: 1 0 100%;
padding-bottom: 10px; padding-bottom: 10px;
@ -70,11 +48,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
.extra-info { .extra-info {
margin-top: 8px; margin-top: 8px;
} }
paper-icon-item {
cursor: pointer;
padding-top: 4px;
padding-bottom: 4px;
}
.manuf, .manuf,
.entity-id, .entity-id,
.area { .area {
@ -82,15 +55,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
} }
</style> </style>
<ha-card> <ha-card>
<div class="card-header">
<template is="dom-if" if="[[!hideSettings]]">
<div class="name">[[_deviceName(device)]]</div>
<paper-icon-button
icon="hass:settings"
on-click="_gotoSettings"
></paper-icon-button>
</template>
</div>
<div class="card-content"> <div class="card-content">
<div class="info"> <div class="info">
<div class="model">[[device.model]]</div> <div class="model">[[device.model]]</div>
@ -122,27 +86,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
</div> </div>
</template> </template>
</div> </div>
<template is="dom-if" if="[[!hideEntities]]">
<div class="device-entities">
<template
is="dom-repeat"
items="[[_computeDeviceEntities(hass, device, entities)]]"
as="entity"
>
<paper-icon-item on-click="_openMoreInfo">
<state-badge
state-obj="[[_computeStateObj(entity, hass)]]"
slot="item-icon"
></state-badge>
<paper-item-body>
<div class="name">[[_computeEntityName(entity, hass)]]</div>
<div class="secondary entity-id">[[entity.entity_id]]</div>
</paper-item-body>
</paper-icon-item>
</template>
</div>
</template>
</ha-card> </ha-card>
`; `;
} }
@ -152,14 +95,11 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
device: Object, device: Object,
devices: Array, devices: Array,
areas: Array, areas: Array,
entities: Array,
hass: Object, hass: Object,
narrow: { narrow: {
type: Boolean, type: Boolean,
reflectToAttribute: true, reflectToAttribute: true,
}, },
hideSettings: { type: Boolean, value: false },
hideEntities: { type: Boolean, value: false },
_childDevices: { _childDevices: {
type: Array, type: Array,
computed: "_computeChildDevices(device, devices)", computed: "_computeChildDevices(device, devices)",
@ -172,30 +112,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
loadDeviceRegistryDetailDialog(); loadDeviceRegistryDetailDialog();
} }
connectedCallback() {
super.connectedCallback();
this._unsubAreas = subscribeAreaRegistry(this.hass.connection, (areas) => {
this._areas = areas;
});
this._unsubDevices = subscribeDeviceRegistry(
this.hass.connection,
(devices) => {
this.devices = devices;
this.device = devices.find((device) => device.id === this.device.id);
}
);
}
disconnectedCallback() {
super.disconnectedCallback();
if (this._unsubAreas) {
this._unsubAreas();
}
if (this._unsubDevices) {
this._unsubDevices();
}
}
_computeArea(areas, device) { _computeArea(areas, device) {
if (!areas || !device || !device.area_id) { if (!areas || !device || !device.area_id) {
return "No Area"; return "No Area";
@ -210,30 +126,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
.sort((dev1, dev2) => compare(dev1.name, dev2.name)); .sort((dev1, dev2) => compare(dev1.name, dev2.name));
} }
_computeDeviceEntities(hass, device, entities) {
return entities
.filter((entity) => entity.device_id === device.id)
.sort((ent1, ent2) =>
compare(
computeEntityName(hass, ent1) || `zzz${ent1.entity_id}`,
computeEntityName(hass, ent2) || `zzz${ent2.entity_id}`
)
);
}
_computeStateObj(entity, hass) {
return hass.states[entity.entity_id];
}
_computeEntityName(entity, hass) {
return (
computeEntityName(hass, entity) ||
`(${this.localize(
"ui.panel.config.integrations.config_entry.entity_unavailable"
)})`
);
}
_deviceName(device) { _deviceName(device) {
return device.name_by_user || device.name; return device.name_by_user || device.name;
} }

View File

@ -13,6 +13,7 @@ import "../../../layouts/hass-subpage";
import "../../../layouts/hass-error-screen"; import "../../../layouts/hass-error-screen";
import "../ha-config-section"; import "../ha-config-section";
import "./device-detail/ha-device-card";
import "./device-detail/ha-device-triggers-card"; import "./device-detail/ha-device-triggers-card";
import "./device-detail/ha-device-conditions-card"; import "./device-detail/ha-device-conditions-card";
import "./device-detail/ha-device-actions-card"; import "./device-detail/ha-device-actions-card";
@ -144,9 +145,6 @@ export class HaConfigDevicePage extends LitElement {
.areas=${this.areas} .areas=${this.areas}
.devices=${this.devices} .devices=${this.devices}
.device=${device} .device=${device}
.entities=${this.entities}
hide-settings
hide-entities
></ha-device-card> ></ha-device-card>
${entities.length ${entities.length

View File

@ -1,19 +1,5 @@
import "@polymer/paper-tooltip/paper-tooltip";
import "@material/mwc-button";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "../../../components/ha-card";
import "../../../components/data-table/ha-data-table";
import "../../../components/entity/ha-state-icon";
import "../../../layouts/hass-subpage"; import "../../../layouts/hass-subpage";
import "../../../resources/ha-style"; import "./ha-devices-data-table";
import "../../../components/ha-icon-next";
import "../ha-config-section";
import memoizeOne from "memoize-one";
import { import {
LitElement, LitElement,
@ -21,33 +7,14 @@ import {
TemplateResult, TemplateResult,
property, property,
customElement, customElement,
CSSResult,
css,
} from "lit-element"; } from "lit-element";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
// tslint:disable-next-line
import {
DataTabelColumnContainer,
RowClickedEvent,
DataTabelRowData,
} from "../../../components/data-table/ha-data-table";
// tslint:disable-next-line
import { DeviceRegistryEntry } from "../../../data/device_registry"; import { DeviceRegistryEntry } from "../../../data/device_registry";
import { EntityRegistryEntry } from "../../../data/entity_registry"; import { EntityRegistryEntry } from "../../../data/entity_registry";
import { ConfigEntry } from "../../../data/config_entries"; import { ConfigEntry } from "../../../data/config_entries";
import { AreaRegistryEntry } from "../../../data/area_registry"; import { AreaRegistryEntry } from "../../../data/area_registry";
import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize";
import { computeStateName } from "../../../common/entity/compute_state_name";
interface DeviceRowData extends DeviceRegistryEntry {
device?: DeviceRowData;
area?: string;
integration?: string;
battery_entity?: string;
}
interface DeviceEntityLookup {
[deviceId: string]: EntityRegistryEntry[];
}
@customElement("ha-config-devices-dashboard") @customElement("ha-config-devices-dashboard")
export class HaConfigDeviceDashboard extends LitElement { export class HaConfigDeviceDashboard extends LitElement {
@ -59,234 +26,35 @@ export class HaConfigDeviceDashboard extends LitElement {
@property() public areas!: AreaRegistryEntry[]; @property() public areas!: AreaRegistryEntry[];
@property() public domain!: string; @property() public domain!: string;
private _devices = memoizeOne(
(
devices: DeviceRegistryEntry[],
entries: ConfigEntry[],
entities: EntityRegistryEntry[],
areas: AreaRegistryEntry[],
domain: string,
localize: LocalizeFunc
) => {
// Some older installations might have devices pointing at invalid entryIDs
// So we guard for that.
let outputDevices: DeviceRowData[] = devices;
const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {};
for (const device of devices) {
deviceLookup[device.id] = device;
}
const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {
deviceEntityLookup[entity.device_id] = [];
}
deviceEntityLookup[entity.device_id].push(entity);
}
const entryLookup: { [entryId: string]: ConfigEntry } = {};
for (const entry of entries) {
entryLookup[entry.entry_id] = entry;
}
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
for (const area of areas) {
areaLookup[area.area_id] = area;
}
if (domain) {
outputDevices = outputDevices.filter((device) =>
device.config_entries.find(
(entryId) =>
entryId in entryLookup && entryLookup[entryId].domain === domain
)
);
}
outputDevices = outputDevices.map((device) => {
return {
...device,
name:
device.name_by_user ||
device.name ||
this._fallbackDeviceName(device.id, deviceEntityLookup) ||
"No name",
model: device.model || "<unknown>",
manufacturer: device.manufacturer || "<unknown>",
area: device.area_id ? areaLookup[device.area_id].name : "No area",
integration: device.config_entries.length
? device.config_entries
.filter((entId) => entId in entryLookup)
.map(
(entId) =>
localize(
`component.${entryLookup[entId].domain}.config.title`
) || entryLookup[entId].domain
)
.join(", ")
: "No integration",
battery_entity: this._batteryEntity(device.id, deviceEntityLookup),
};
});
return outputDevices;
}
);
private _columns = memoizeOne(
(narrow: boolean): DataTabelColumnContainer =>
narrow
? {
device: {
title: "Device",
sortable: true,
filterKey: "name",
filterable: true,
direction: "asc",
template: (device: DeviceRowData) => {
const battery = device.battery_entity
? this.hass.states[device.battery_entity]
: undefined;
// Have to work on a nice layout for mobile
return html`
${device.name_by_user || device.name}<br />
${device.area} | ${device.integration}<br />
${battery
? html`
${battery.state}%
<ha-state-icon
.hass=${this.hass!}
.stateObj=${battery}
></ha-state-icon>
`
: ""}
`;
},
},
}
: {
device_name: {
title: "Device",
sortable: true,
filterable: true,
direction: "asc",
},
manufacturer: {
title: "Manufacturer",
sortable: true,
filterable: true,
},
model: {
title: "Model",
sortable: true,
filterable: true,
},
area: {
title: "Area",
sortable: true,
filterable: true,
},
integration: {
title: "Integration",
sortable: true,
filterable: true,
},
battery: {
title: "Battery",
sortable: true,
type: "numeric",
template: (batteryEntity: string) => {
const battery = batteryEntity
? this.hass.states[batteryEntity]
: undefined;
return battery
? html`
${battery.state}%
<ha-state-icon
.hass=${this.hass!}
.stateObj=${battery}
></ha-state-icon>
`
: html`
-
`;
},
},
}
);
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<hass-subpage <hass-subpage
header=${this.hass.localize("ui.panel.config.devices.caption")} header=${this.hass.localize("ui.panel.config.devices.caption")}
> >
<ha-data-table <div class="content">
.columns=${this._columns(this.narrow)} <ha-devices-data-table
.data=${this._devices( .hass=${this.hass}
this.devices, .narrow=${this.narrow}
this.entries, .devices=${this.devices}
this.entities, .entries=${this.entries}
this.areas, .entities=${this.entities}
this.domain, .areas=${this.areas}
this.hass.localize .domain=${this.domain}
).map((device: DeviceRowData) => { ></ha-devices-data-table>
// We don't need a lot of this data for mobile view, but kept it for filtering... </div>
const data: DataTabelRowData = {
device_name: device.name,
id: device.id,
manufacturer: device.manufacturer,
model: device.model,
area: device.area,
integration: device.integration,
};
if (this.narrow) {
data.device = device;
return data;
}
data.battery = device.battery_entity;
return data;
})}
@row-click=${this._handleRowClicked}
></ha-data-table>
</hass-subpage> </hass-subpage>
`; `;
} }
private _batteryEntity( static get styles(): CSSResult {
deviceId: string, return css`
deviceEntityLookup: DeviceEntityLookup .content {
): string | undefined { padding: 4px;
const batteryEntity = (deviceEntityLookup[deviceId] || []).find(
(entity) =>
this.hass.states[entity.entity_id] &&
this.hass.states[entity.entity_id].attributes.device_class === "battery"
);
return batteryEntity ? batteryEntity.entity_id : undefined;
}
private _fallbackDeviceName(
deviceId: string,
deviceEntityLookup: DeviceEntityLookup
): string | undefined {
for (const entity of deviceEntityLookup[deviceId] || []) {
const stateObj = this.hass.states[entity.entity_id];
if (stateObj) {
return computeStateName(stateObj);
} }
} ha-devices-data-table {
width: 100%;
return undefined; }
} `;
private _handleRowClicked(ev: CustomEvent) {
const deviceId = (ev.detail as RowClickedEvent).id;
navigate(this, `/config/devices/device/${deviceId}`);
} }
} }

View File

@ -0,0 +1,265 @@
import "../../../components/data-table/ha-data-table";
import "../../../components/entity/ha-state-icon";
import memoizeOne from "memoize-one";
import {
LitElement,
html,
TemplateResult,
property,
customElement,
} from "lit-element";
import { HomeAssistant } from "../../../types";
// tslint:disable-next-line
import {
DataTableColumnContainer,
RowClickedEvent,
DataTableRowData,
} from "../../../components/data-table/ha-data-table";
// tslint:disable-next-line
import { DeviceRegistryEntry } from "../../../data/device_registry";
import { EntityRegistryEntry } from "../../../data/entity_registry";
import { ConfigEntry } from "../../../data/config_entries";
import { AreaRegistryEntry } from "../../../data/area_registry";
import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize";
import { computeStateName } from "../../../common/entity/compute_state_name";
export interface DeviceRowData extends DeviceRegistryEntry {
device?: DeviceRowData;
area?: string;
integration?: string;
battery_entity?: string;
}
export interface DeviceEntityLookup {
[deviceId: string]: EntityRegistryEntry[];
}
@customElement("ha-devices-data-table")
export class HaDevicesDataTable extends LitElement {
@property() public hass!: HomeAssistant;
@property() public narrow = false;
@property() public devices!: DeviceRegistryEntry[];
@property() public entries!: ConfigEntry[];
@property() public entities!: EntityRegistryEntry[];
@property() public areas!: AreaRegistryEntry[];
@property() public domain!: string;
private _devices = memoizeOne(
(
devices: DeviceRegistryEntry[],
entries: ConfigEntry[],
entities: EntityRegistryEntry[],
areas: AreaRegistryEntry[],
domain: string,
localize: LocalizeFunc
) => {
// Some older installations might have devices pointing at invalid entryIDs
// So we guard for that.
let outputDevices: DeviceRowData[] = devices;
const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {};
for (const device of devices) {
deviceLookup[device.id] = device;
}
const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {
deviceEntityLookup[entity.device_id] = [];
}
deviceEntityLookup[entity.device_id].push(entity);
}
const entryLookup: { [entryId: string]: ConfigEntry } = {};
for (const entry of entries) {
entryLookup[entry.entry_id] = entry;
}
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
for (const area of areas) {
areaLookup[area.area_id] = area;
}
if (domain) {
outputDevices = outputDevices.filter((device) =>
device.config_entries.find(
(entryId) =>
entryId in entryLookup && entryLookup[entryId].domain === domain
)
);
}
outputDevices = outputDevices.map((device) => {
return {
...device,
name:
device.name_by_user ||
device.name ||
this._fallbackDeviceName(device.id, deviceEntityLookup) ||
"No name",
model: device.model || "<unknown>",
manufacturer: device.manufacturer || "<unknown>",
area: device.area_id ? areaLookup[device.area_id].name : "No area",
integration: device.config_entries.length
? device.config_entries
.filter((entId) => entId in entryLookup)
.map(
(entId) =>
localize(
`component.${entryLookup[entId].domain}.config.title`
) || entryLookup[entId].domain
)
.join(", ")
: "No integration",
battery_entity: this._batteryEntity(device.id, deviceEntityLookup),
};
});
return outputDevices;
}
);
private _columns = memoizeOne(
(narrow: boolean): DataTableColumnContainer =>
narrow
? {
name: {
title: "Device",
sortable: true,
filterKey: "name",
filterable: true,
direction: "asc",
template: (name, device: DataTableRowData) => {
const battery = device.battery_entity
? this.hass.states[device.battery_entity]
: undefined;
// Have to work on a nice layout for mobile
return html`
${name}<br />
${device.area} | ${device.integration}<br />
${battery
? html`
${battery.state}%
<ha-state-icon
.hass=${this.hass!}
.stateObj=${battery}
></ha-state-icon>
`
: ""}
`;
},
},
}
: {
name: {
title: "Device",
sortable: true,
filterable: true,
direction: "asc",
},
manufacturer: {
title: "Manufacturer",
sortable: true,
filterable: true,
},
model: {
title: "Model",
sortable: true,
filterable: true,
},
area: {
title: "Area",
sortable: true,
filterable: true,
},
integration: {
title: "Integration",
sortable: true,
filterable: true,
},
battery_entity: {
title: "Battery",
sortable: true,
type: "numeric",
template: (batteryEntity: string) => {
const battery = batteryEntity
? this.hass.states[batteryEntity]
: undefined;
return battery
? html`
${battery.state}%
<ha-state-icon
.hass=${this.hass!}
.stateObj=${battery}
></ha-state-icon>
`
: html`
-
`;
},
},
}
);
protected render(): TemplateResult {
return html`
<ha-data-table
.columns=${this._columns(this.narrow)}
.data=${this._devices(
this.devices,
this.entries,
this.entities,
this.areas,
this.domain,
this.hass.localize
)}
@row-click=${this._handleRowClicked}
></ha-data-table>
`;
}
private _batteryEntity(
deviceId: string,
deviceEntityLookup: DeviceEntityLookup
): string | undefined {
const batteryEntity = (deviceEntityLookup[deviceId] || []).find(
(entity) =>
this.hass.states[entity.entity_id] &&
this.hass.states[entity.entity_id].attributes.device_class === "battery"
);
return batteryEntity ? batteryEntity.entity_id : undefined;
}
private _fallbackDeviceName(
deviceId: string,
deviceEntityLookup: DeviceEntityLookup
): string | undefined {
for (const entity of deviceEntityLookup[deviceId] || []) {
const stateObj = this.hass.states[entity.entity_id];
if (stateObj) {
return computeStateName(stateObj);
}
}
return undefined;
}
private _handleRowClicked(ev: CustomEvent) {
const deviceId = (ev.detail as RowClickedEvent).id;
navigate(this, `/config/devices/device/${deviceId}`);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-devices-data-table": HaDevicesDataTable;
}
}

View File

@ -6,9 +6,6 @@ import {
CSSResult, CSSResult,
property, property,
} from "lit-element"; } from "lit-element";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { import {
@ -18,7 +15,7 @@ import {
} from "../../../data/entity_registry"; } from "../../../data/entity_registry";
import "../../../layouts/hass-subpage"; import "../../../layouts/hass-subpage";
import "../../../layouts/hass-loading-screen"; import "../../../layouts/hass-loading-screen";
import "../../../components/ha-card"; import "../../../components/data-table/ha-data-table";
import "../../../components/ha-icon"; import "../../../components/ha-icon";
import "../../../components/ha-switch"; import "../../../components/ha-switch";
import { domainIcon } from "../../../common/entity/domain_icon"; import { domainIcon } from "../../../common/entity/domain_icon";
@ -30,11 +27,14 @@ import {
loadEntityRegistryDetailDialog, loadEntityRegistryDetailDialog,
} from "./show-dialog-entity-registry-detail"; } from "./show-dialog-entity-registry-detail";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { compare } from "../../../common/string/compare";
import { classMap } from "lit-html/directives/class-map";
// tslint:disable-next-line // tslint:disable-next-line
import { HaSwitch } from "../../../components/ha-switch"; import { HaSwitch } from "../../../components/ha-switch";
import memoize from "memoize-one"; import memoize from "memoize-one";
// tslint:disable-next-line
import {
DataTableColumnContainer,
RowClickedEvent,
} from "../../../components/data-table/ha-data-table";
class HaConfigEntityRegistry extends LitElement { class HaConfigEntityRegistry extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@ -43,11 +43,76 @@ class HaConfigEntityRegistry extends LitElement {
@property() private _showDisabled = false; @property() private _showDisabled = false;
private _unsubEntities?: UnsubscribeFunc; private _unsubEntities?: UnsubscribeFunc;
private _columns = memoize(
(_language): DataTableColumnContainer => {
return {
icon: {
title: "",
type: "icon",
template: (icon) => html`
<ha-icon slot="item-icon" .icon=${icon}></ha-icon>
`,
},
name: {
title: this.hass.localize(
"ui.panel.config.entity_registry.picker.headers.name"
),
sortable: true,
filterable: true,
direction: "asc",
},
entity_id: {
title: this.hass.localize(
"ui.panel.config.entity_registry.picker.headers.entity_id"
),
sortable: true,
filterable: true,
},
platform: {
title: this.hass.localize(
"ui.panel.config.entity_registry.picker.headers.integration"
),
sortable: true,
filterable: true,
template: (platform) =>
html`
${this.hass.localize(`component.${platform}.config.title`) ||
platform}
`,
},
disabled_by: {
title: this.hass.localize(
"ui.panel.config.entity_registry.picker.headers.enabled"
),
type: "icon",
template: (disabledBy) => html`
<ha-icon
slot="item-icon"
.icon=${disabledBy ? "hass:cancel" : "hass:check-circle"}
></ha-icon>
`,
},
};
}
);
private _filteredEntities = memoize( private _filteredEntities = memoize(
(entities: EntityRegistryEntry[], showDisabled: boolean) => (entities: EntityRegistryEntry[], showDisabled: boolean) =>
showDisabled (showDisabled
? entities ? entities
: entities.filter((entity) => !Boolean(entity.disabled_by)) : entities.filter((entity) => !Boolean(entity.disabled_by))
).map((entry) => {
const state = this.hass!.states[entry.entity_id];
return {
...entry,
icon: state
? stateIcon(state)
: domainIcon(computeDomain(entry.entity_id)),
name:
computeEntityRegistryName(this.hass!, entry) ||
this.hass!.localize("state.default.unavailable"),
};
})
); );
public disconnectedCallback() { public disconnectedCallback() {
@ -89,56 +154,21 @@ class HaConfigEntityRegistry extends LitElement {
"ui.panel.config.entity_registry.picker.integrations_page" "ui.panel.config.entity_registry.picker.integrations_page"
)} )}
</a> </a>
</span> <ha-switch
<ha-card> ?checked=${this._showDisabled}
<paper-item> @change=${this._showDisabledChanged}
<ha-switch >${this.hass.localize(
?checked=${this._showDisabled} "ui.panel.config.entity_registry.picker.show_disabled"
@change=${this._showDisabledChanged} )}</ha-switch
>${this.hass.localize(
"ui.panel.config.entity_registry.picker.show_disabled"
)}</ha-switch
></paper-item
> >
${this._filteredEntities(this._entities, this._showDisabled).map( </span>
(entry) => { <ha-data-table
const state = this.hass!.states[entry.entity_id]; .columns=${this._columns(this.hass.language)}
return html` .data=${this._filteredEntities(this._entities, this._showDisabled)}
<paper-icon-item @row-click=${this._openEditEntry}
@click=${this._openEditEntry} id="entity_id"
.entry=${entry} >
class=${classMap({ "disabled-entry": !!entry.disabled_by })} </ha-data-table>
>
<ha-icon
slot="item-icon"
.icon=${state
? stateIcon(state)
: domainIcon(computeDomain(entry.entity_id))}
></ha-icon>
<paper-item-body two-line>
<div class="name">
${computeEntityRegistryName(this.hass!, entry) ||
`(${this.hass!.localize(
"state.default.unavailable"
)})`}
</div>
<div class="secondary entity-id">
${entry.entity_id}
</div>
</paper-item-body>
<div class="platform">
${entry.platform}
${entry.disabled_by
? html`
<br />(disabled)
`
: ""}
</div>
</paper-icon-item>
`;
}
)}
</ha-card>
</ha-config-section> </ha-config-section>
</hass-subpage> </hass-subpage>
`; `;
@ -155,9 +185,7 @@ class HaConfigEntityRegistry extends LitElement {
this._unsubEntities = subscribeEntityRegistry( this._unsubEntities = subscribeEntityRegistry(
this.hass.connection, this.hass.connection,
(entities) => { (entities) => {
this._entities = entities.sort((ent1, ent2) => this._entities = entities;
compare(ent1.entity_id, ent2.entity_id)
);
} }
); );
} }
@ -167,8 +195,14 @@ class HaConfigEntityRegistry extends LitElement {
this._showDisabled = (ev.target as HaSwitch).checked; this._showDisabled = (ev.target as HaSwitch).checked;
} }
private _openEditEntry(ev: MouseEvent): void { private _openEditEntry(ev: CustomEvent): void {
const entry = (ev.currentTarget! as any).entry; const entryId = (ev.detail as RowClickedEvent).id;
const entry = this._entities!.find(
(entity) => entity.entity_id === entryId
);
if (!entry) {
return;
}
showEntityRegistryDetailDialog(this, { showEntityRegistryDetailDialog(this, {
entry, entry,
}); });
@ -179,23 +213,12 @@ class HaConfigEntityRegistry extends LitElement {
a { a {
color: var(--primary-color); color: var(--primary-color);
} }
ha-card { ha-data-table {
margin-bottom: 24px; margin-bottom: 24px;
direction: ltr; margin-top: 0px;
} }
paper-icon-item { ha-switch {
cursor: pointer; margin-top: 16px;
color: var(--primary-text-color);
}
ha-icon {
margin-left: 8px;
}
.platform {
text-align: right;
margin: 0 0 0 8px;
}
.disabled-entry {
color: var(--secondary-text-color);
} }
`; `;
} }

View File

@ -20,7 +20,7 @@ class HaCeEntitiesCard extends LocalizeMixIn(EventsMixin(PolymerElement)) {
return html` return html`
<style> <style>
ha-card { ha-card {
flex: 1 0 100%; margin-top: 8px;
padding-bottom: 8px; padding-bottom: 8px;
} }
paper-icon-item { paper-icon-item {

View File

@ -2,10 +2,7 @@ import memoizeOne from "memoize-one";
import "../../../../layouts/hass-subpage"; import "../../../../layouts/hass-subpage";
import "../../../../layouts/hass-error-screen"; import "../../../../layouts/hass-error-screen";
import "../../../../components/entity/state-badge"; import "../../devices/ha-devices-data-table";
import { compare } from "../../../../common/string/compare";
import "../../devices/device-detail/ha-device-card";
import "./ha-ce-entities-card"; import "./ha-ce-entities-card";
import { showOptionsFlowDialog } from "../../../../dialogs/config-flow/show-dialog-options-flow"; import { showOptionsFlowDialog } from "../../../../dialogs/config-flow/show-dialog-options-flow";
import { property, LitElement, CSSResult, css, html } from "lit-element"; import { property, LitElement, CSSResult, css, html } from "lit-element";
@ -43,15 +40,9 @@ class HaConfigEntryPage extends LitElement {
if (!devices) { if (!devices) {
return []; return [];
} }
return devices return devices.filter((device) =>
.filter((device) => device.config_entries.includes(configEntry.entry_id)
device.config_entries.includes(configEntry.entry_id) );
)
.sort(
(dev1, dev2) =>
Number(!!dev1.via_device_id) - Number(!!dev2.via_device_id) ||
compare(dev1.name || "", dev2.name || "")
);
} }
); );
@ -116,24 +107,19 @@ class HaConfigEntryPage extends LitElement {
)} )}
</p> </p>
` `
: ""} : html`
${configEntryDevices.map( <ha-devices-data-table
(device) => html` .hass=${this.hass}
<ha-device-card .narrow=${this.narrow}
class="card" .devices=${configEntryDevices}
.hass=${this.hass} .entries=${this.configEntries}
.areas=${this.areas} .entities=${this.entityRegistryEntries}
.devices=${this.deviceRegistryEntries} .areas=${this.areas}
.device=${device} ></ha-devices-data-table>
.entities=${this.entityRegistryEntries} `}
.narrow=${this.narrow}
></ha-device-card>
`
)}
${noDeviceEntities.length > 0 ${noDeviceEntities.length > 0
? html` ? html`
<ha-ce-entities-card <ha-ce-entities-card
class="card"
.heading=${this.hass.localize( .heading=${this.hass.localize(
"ui.panel.config.integrations.config_entry.no_device" "ui.panel.config.integrations.config_entry.no_device"
)} )}
@ -185,18 +171,13 @@ class HaConfigEntryPage extends LitElement {
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
.content { .content {
display: flex;
flex-wrap: wrap;
padding: 4px; padding: 4px;
justify-content: center;
} }
.card { p {
box-sizing: border-box; text-align: center;
display: flex; }
flex: 1 0 300px; ha-devices-data-table {
min-width: 0; width: 100%;
max-width: 500px;
padding: 8px;
} }
`; `;
} }

View File

@ -2,29 +2,49 @@ import { h, Component } from "preact";
import "../../../../components/device/ha-device-picker"; import "../../../../components/device/ha-device-picker";
import "../../../../components/device/ha-device-condition-picker"; import "../../../../components/device/ha-device-condition-picker";
import "../../../../components/ha-form";
import {
fetchDeviceConditionCapabilities,
deviceAutomationsEqual,
} from "../../../../data/device_automation";
export default class DeviceCondition extends Component<any, any> { export default class DeviceCondition extends Component<any, any> {
private _origCondition;
constructor() { constructor() {
super(); super();
this.devicePicked = this.devicePicked.bind(this); this.devicePicked = this.devicePicked.bind(this);
this.deviceConditionPicked = this.deviceConditionPicked.bind(this); this.deviceConditionPicked = this.deviceConditionPicked.bind(this);
this.state = { device_id: undefined }; this._extraFieldsChanged = this._extraFieldsChanged.bind(this);
this.state = { device_id: undefined, capabilities: undefined };
} }
public devicePicked(ev) { public devicePicked(ev) {
this.setState({ device_id: ev.target.value }); this.setState({ ...this.state, device_id: ev.target.value });
} }
public deviceConditionPicked(ev) { public deviceConditionPicked(ev) {
const deviceCondition = ev.target.value; let condition = ev.target.value;
this.props.onChange(this.props.index, deviceCondition); if (
this._origCondition &&
deviceAutomationsEqual(this._origCondition, condition)
) {
condition = this._origCondition;
}
this.props.onChange(this.props.index, condition);
} }
/* eslint-disable camelcase */ /* eslint-disable camelcase */
public render({ condition, hass }, { device_id }) { public render({ condition, hass }, { device_id, capabilities }) {
if (device_id === undefined) { if (device_id === undefined) {
device_id = condition.device_id; device_id = condition.device_id;
} }
const extraFieldsData =
capabilities && capabilities.extra_fields
? capabilities.extra_fields.map((item) => {
return { [item.name]: this.props.condition[item.name] };
})
: undefined;
return ( return (
<div> <div>
@ -41,9 +61,64 @@ export default class DeviceCondition extends Component<any, any> {
hass={hass} hass={hass}
label="Condition" label="Condition"
/> />
{extraFieldsData && (
<ha-form
data={Object.assign({}, ...extraFieldsData)}
onData-changed={this._extraFieldsChanged}
schema={this.state.capabilities.extra_fields}
computeLabel={this._extraFieldsComputeLabelCallback(hass.localize)}
/>
)}
</div> </div>
); );
} }
public componentDidMount() {
if (!this.state.capabilities) {
this._getCapabilities();
}
if (this.props.condition) {
this._origCondition = this.props.condition;
}
}
public componentDidUpdate(prevProps) {
if (prevProps.condition !== this.props.condition) {
this._getCapabilities();
}
}
private async _getCapabilities() {
const condition = this.props.condition;
const capabilities = condition.domain
? await fetchDeviceConditionCapabilities(this.props.hass, condition)
: null;
this.setState({ ...this.state, capabilities });
}
private _extraFieldsChanged(ev) {
if (!ev.detail.path) {
return;
}
const item = ev.detail.path.replace("data.", "");
const value = ev.detail.value || undefined;
this.props.onChange(this.props.index, {
...this.props.condition,
[item]: value,
});
}
private _extraFieldsComputeLabelCallback(localize) {
// Returns a callback for ha-form to calculate labels per schema object
return (schema) =>
localize(
`ui.panel.config.automation.editor.condition.type.device.extra_fields.${
schema.name
}`
) || schema.name;
}
} }
(DeviceCondition as any).defaultConfig = { (DeviceCondition as any).defaultConfig = {

View File

@ -20,6 +20,7 @@ declare global {
"ha-device-picker": any; "ha-device-picker": any;
"ha-device-condition-picker": any; "ha-device-condition-picker": any;
"ha-textarea": any; "ha-textarea": any;
"ha-code-editor": any;
"ha-service-picker": any; "ha-service-picker": any;
"mwc-button": any; "mwc-button": any;
"ha-device-trigger-picker": any; "ha-device-trigger-picker": any;

View File

@ -8,6 +8,7 @@ import ConditionAction from "./condition";
import DelayAction from "./delay"; import DelayAction from "./delay";
import DeviceAction from "./device"; import DeviceAction from "./device";
import EventAction from "./event"; import EventAction from "./event";
import SceneAction from "./scene";
import WaitAction from "./wait"; import WaitAction from "./wait";
const TYPES = { const TYPES = {
@ -17,6 +18,7 @@ const TYPES = {
condition: ConditionAction, condition: ConditionAction,
event: EventAction, event: EventAction,
device_id: DeviceAction, device_id: DeviceAction,
scene: SceneAction,
}; };
const OPTIONS = Object.keys(TYPES).sort(); const OPTIONS = Object.keys(TYPES).sort();

View File

@ -0,0 +1,38 @@
import { h, Component } from "preact";
import "../../../../components/entity/ha-entity-picker";
export default class SceneAction extends Component<any> {
constructor() {
super();
this.sceneChanged = this.sceneChanged.bind(this);
}
public sceneChanged(ev: any) {
this.props.onChange(this.props.index, {
...this.props.action,
scene: ev.target.value,
});
}
public render({ action, hass }) {
const { scene } = action;
return (
<div>
<ha-entity-picker
value={scene}
onChange={this.sceneChanged}
hass={hass}
domainFilter="scene"
allowCustomEntity
/>
</div>
);
}
}
(SceneAction as any).defaultConfig = {
alias: "",
scene: "",
};

View File

@ -1,6 +1,6 @@
import { h, Component } from "preact"; import { h, Component } from "preact";
import yaml from "js-yaml"; import yaml from "js-yaml";
import "../../../components/ha-textarea"; import "../../../components/ha-code-editor";
const isEmpty = (obj: object) => { const isEmpty = (obj: object) => {
for (const key in obj) { for (const key in obj) {
@ -34,7 +34,7 @@ export default class YAMLTextArea extends Component<any, any> {
} }
public onChange(ev) { public onChange(ev) {
const value = ev.target.value; const value = ev.detail.value;
let parsed; let parsed;
let isValid = true; let isValid = true;
@ -64,17 +64,17 @@ export default class YAMLTextArea extends Component<any, any> {
minWidth: 300, minWidth: 300,
width: "100%", width: "100%",
}; };
if (!isValid) {
style.border = "1px solid red";
}
return ( return (
<ha-textarea <div>
label={label} <p>{label}</p>
value={value} <ha-code-editor
style={style} mode="yaml"
onvalue-changed={this.onChange} style={style}
dir="ltr" value={value}
/> error={isValid === false}
onvalue-changed={this.onChange}
/>
</div>
); );
} }
} }

View File

@ -9,9 +9,9 @@ import {
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@material/mwc-button"; import "@material/mwc-button";
import memoizeOne from "memoize-one";
import "../../../components/dialog/ha-paper-dialog"; import "../../../components/dialog/ha-paper-dialog";
import "../../../components/entity/ha-entities-picker"; import "../../../components/entity/ha-entities-picker";
import "../../../components/user/ha-user-picker"; import "../../../components/user/ha-user-picker";
import { PersonDetailDialogParams } from "./show-dialog-person-detail"; import { PersonDetailDialogParams } from "./show-dialog-person-detail";
@ -29,6 +29,13 @@ class DialogPersonDetail extends LitElement {
@property() private _params?: PersonDetailDialogParams; @property() private _params?: PersonDetailDialogParams;
@property() private _submitting: boolean = false; @property() private _submitting: boolean = false;
private _deviceTrackersAvailable = memoizeOne((hass) => {
return Object.keys(hass.states).some(
(entityId) =>
entityId.substr(0, entityId.indexOf(".")) === "device_tracker"
);
});
public async showDialog(params: PersonDetailDialogParams): Promise<void> { public async showDialog(params: PersonDetailDialogParams): Promise<void> {
this._params = params; this._params = params;
this._error = undefined; this._error = undefined;
@ -55,7 +62,11 @@ class DialogPersonDetail extends LitElement {
opened opened
@opened-changed="${this._openedChanged}" @opened-changed="${this._openedChanged}"
> >
<h2>${this._params.entry ? this._params.entry.name : "New Person"}</h2> <h2>
${this._params.entry
? this._params.entry.name
: this.hass!.localize("ui.panel.config.person.detail.new_person")}
</h2>
<paper-dialog-scrollable> <paper-dialog-scrollable>
${this._error ${this._error
? html` ? html`
@ -66,34 +77,72 @@ class DialogPersonDetail extends LitElement {
<paper-input <paper-input
.value=${this._name} .value=${this._name}
@value-changed=${this._nameChanged} @value-changed=${this._nameChanged}
label="Name" label="${this.hass!.localize(
error-message="Name is required" "ui.panel.config.person.detail.name"
)}"
error-message="${this.hass!.localize(
"ui.panel.config.person.detail.name_error_msg"
)}"
.invalid=${nameInvalid} .invalid=${nameInvalid}
></paper-input> ></paper-input>
<ha-user-picker <ha-user-picker
label="Linked User" label="${this.hass!.localize(
"ui.panel.config.person.detail.linked_user"
)}"
.hass=${this.hass} .hass=${this.hass}
.value=${this._userId} .value=${this._userId}
.users=${this._params.users} .users=${this._params.users}
@value-changed=${this._userChanged} @value-changed=${this._userChanged}
></ha-user-picker> ></ha-user-picker>
<p> ${this._deviceTrackersAvailable(this.hass)
${this.hass.localize( ? html`
"ui.panel.config.person.detail.device_tracker_intro" <p>
)} ${this.hass.localize(
</p> "ui.panel.config.person.detail.device_tracker_intro"
<ha-entities-picker )}
.hass=${this.hass} </p>
.value=${this._deviceTrackers} <ha-entities-picker
domain-filter="device_tracker" .hass=${this.hass}
.pickedEntityLabel=${this.hass.localize( .value=${this._deviceTrackers}
"ui.panel.config.person.detail.device_tracker_picked" domain-filter="device_tracker"
)} .pickedEntityLabel=${this.hass.localize(
.pickEntityLabel=${this.hass.localize( "ui.panel.config.person.detail.device_tracker_picked"
"ui.panel.config.person.detail.device_tracker_pick" )}
)} .pickEntityLabel=${this.hass.localize(
@value-changed=${this._deviceTrackersChanged} "ui.panel.config.person.detail.device_tracker_pick"
></ha-entities-picker> )}
@value-changed=${this._deviceTrackersChanged}
>
</ha-entities-picker>
`
: html`
<p>
${this.hass!.localize(
"ui.panel.config.person.detail.no_device_tracker_available_intro"
)}
</p>
<ul>
<li>
<a
href="https://www.home-assistant.io/integrations/#presence-detection"
target="_blank"
>${this.hass!.localize(
"ui.panel.config.person.detail.link_presence_detection_integrations"
)}</a
>
</li>
<li>
<a
@click="${this._closeDialog}"
href="/config/integrations"
>
${this.hass!.localize(
"ui.panel.config.person.detail.link_integrations_page"
)}</a
>
</li>
</ul>
`}
</div> </div>
</paper-dialog-scrollable> </paper-dialog-scrollable>
<div class="paper-dialog-buttons"> <div class="paper-dialog-buttons">
@ -104,7 +153,7 @@ class DialogPersonDetail extends LitElement {
@click="${this._deleteEntry}" @click="${this._deleteEntry}"
.disabled=${this._submitting} .disabled=${this._submitting}
> >
DELETE ${this.hass!.localize("ui.panel.config.person.detail.delete")}
</mwc-button> </mwc-button>
` `
: html``} : html``}
@ -112,13 +161,19 @@ class DialogPersonDetail extends LitElement {
@click="${this._updateEntry}" @click="${this._updateEntry}"
.disabled=${nameInvalid || this._submitting} .disabled=${nameInvalid || this._submitting}
> >
${this._params.entry ? "UPDATE" : "CREATE"} ${this._params.entry
? this.hass!.localize("ui.panel.config.person.detail.update")
: this.hass!.localize("ui.panel.config.person.detail.create")}
</mwc-button> </mwc-button>
</div> </div>
</ha-paper-dialog> </ha-paper-dialog>
`; `;
} }
private _closeDialog() {
this._params = undefined;
}
private _nameChanged(ev: PolymerChangedEvent<string>) { private _nameChanged(ev: PolymerChangedEvent<string>) {
this._error = undefined; this._error = undefined;
this._name = ev.detail.value; this._name = ev.detail.value;

View File

@ -55,17 +55,21 @@ class HaConfigPerson extends LitElement {
<hass-loading-screen></hass-loading-screen> <hass-loading-screen></hass-loading-screen>
`; `;
} }
const hass = this.hass;
return html` return html`
<hass-subpage header="Persons"> <hass-subpage header=${hass.localize("ui.panel.config.person.caption")}>
<ha-config-section .isWide=${this.isWide}> <ha-config-section .isWide=${this.isWide}>
<span slot="header">Persons</span> <span slot="header"
>${hass.localize("ui.panel.config.person.caption")}</span
>
<span slot="introduction"> <span slot="introduction">
Here you can define each person of interest in Home Assistant. ${hass.localize("ui.panel.config.person.introduction")}
${this._configItems.length > 0 ${this._configItems.length > 0
? html` ? html`
<p> <p>
Note: persons configured via configuration.yaml cannot be ${hass.localize(
edited via the UI. "ui.panel.config.person.note_about_persons_configured_in_yaml"
)}
</p> </p>
` `
: ""} : ""}
@ -83,9 +87,13 @@ class HaConfigPerson extends LitElement {
${this._storageItems.length === 0 ${this._storageItems.length === 0
? html` ? html`
<div class="empty"> <div class="empty">
Looks like you have not created any persons yet. ${hass.localize(
"ui.panel.config.person.no_persons_created_yet"
)}
<mwc-button @click=${this._createPerson}> <mwc-button @click=${this._createPerson}>
CREATE PERSON</mwc-button ${hass.localize(
"ui.panel.config.person.create_person"
)}</mwc-button
> >
</div> </div>
` `
@ -112,7 +120,7 @@ class HaConfigPerson extends LitElement {
<ha-fab <ha-fab
?is-wide=${this.isWide} ?is-wide=${this.isWide}
icon="hass:plus" icon="hass:plus"
title="Add Person" title="${hass.localize("ui.panel.config.person.add_person")}"
@click=${this._createPerson} @click=${this._createPerson}
></ha-fab> ></ha-fab>
`; `;
@ -180,9 +188,11 @@ class HaConfigPerson extends LitElement {
}, },
removeEntry: async () => { removeEntry: async () => {
if ( if (
!confirm(`Are you sure you want to delete this person? !confirm(`${this.hass!.localize(
"ui.panel.config.person.confirm_delete"
)}
All devices belonging to this person will become unassigned.`) ${this.hass!.localize("ui.panel.config.person.confirm_delete2")}`)
) { ) {
return false; return false;
} }

View File

@ -100,7 +100,7 @@ class HaScriptEditor extends LocalizeMixin(NavigateMixin(PolymerElement)) {
<ha-paper-icon-button-arrow-prev <ha-paper-icon-button-arrow-prev
on-click="backTapped" on-click="backTapped"
></ha-paper-icon-button-arrow-prev> ></ha-paper-icon-button-arrow-prev>
<div main-title>Script [[computeName(script)]]</div> <div main-title>[[computeHeader(script)]]</div>
<template is="dom-if" if="[[!creatingNew]]"> <template is="dom-if" if="[[!creatingNew]]">
<paper-icon-button <paper-icon-button
icon="hass:delete" icon="hass:delete"
@ -120,7 +120,7 @@ class HaScriptEditor extends LocalizeMixin(NavigateMixin(PolymerElement)) {
is-wide$="[[isWide]]" is-wide$="[[isWide]]"
dirty$="[[dirty]]" dirty$="[[dirty]]"
icon="hass:content-save" icon="hass:content-save"
title="Save" title="[[localize('ui.common.save')]]"
on-click="saveScript" on-click="saveScript"
rtl$="[[rtl]]" rtl$="[[rtl]]"
></ha-fab> ></ha-fab>
@ -232,7 +232,11 @@ class HaScriptEditor extends LocalizeMixin(NavigateMixin(PolymerElement)) {
this._updateComponent(); this._updateComponent();
}, },
() => { () => {
alert("Only scripts inside scripts.yaml are editable."); alert(
this.hass.localize(
"ui.panel.config.script.editor.load_error_not_editable"
)
);
history.back(); history.back();
} }
); );
@ -244,7 +248,7 @@ class HaScriptEditor extends LocalizeMixin(NavigateMixin(PolymerElement)) {
} }
this.dirty = false; this.dirty = false;
this.config = { this.config = {
alias: "New Script", alias: this.hass.localize("ui.panel.config.script.editor.default_name"),
sequence: [{ service: "", data: {} }], sequence: [{ service: "", data: {} }],
}; };
this._updateComponent(); this._updateComponent();
@ -254,7 +258,9 @@ class HaScriptEditor extends LocalizeMixin(NavigateMixin(PolymerElement)) {
if ( if (
this.dirty && this.dirty &&
// eslint-disable-next-line // eslint-disable-next-line
!confirm("You have unsaved changes. Are you sure you want to leave?") !confirm(
this.hass.localize("ui.panel.config.common.editor.confirm_unsaved")
)
) { ) {
return; return;
} }
@ -281,7 +287,11 @@ class HaScriptEditor extends LocalizeMixin(NavigateMixin(PolymerElement)) {
} }
async _delete() { async _delete() {
if (!confirm("Are you sure you want to delete this script?")) { if (
!confirm(
this.hass.localize("ui.panel.config.script.editor.delete_confirm")
)
) {
return; return;
} }
await deleteScript(this.hass, computeObjectId(this.script.entity_id)); await deleteScript(this.hass, computeObjectId(this.script.entity_id));
@ -307,8 +317,14 @@ class HaScriptEditor extends LocalizeMixin(NavigateMixin(PolymerElement)) {
); );
} }
computeName(script) { computeHeader(script) {
return script && computeStateName(script); return script
? this.hass.localize(
"ui.panel.config.script.editor.header",
"name",
computeStateName(script)
)
: this.hass.localize("ui.panel.config.script.editor.default_name");
} }
_computeRTL(hass) { _computeRTL(hass) {

View File

@ -38,22 +38,32 @@ class HaScriptPicker extends LitElement {
.header=${this.hass.localize("ui.panel.config.script.caption")} .header=${this.hass.localize("ui.panel.config.script.caption")}
> >
<ha-config-section .isWide=${this.isWide}> <ha-config-section .isWide=${this.isWide}>
<div slot="header">Script Editor</div> <div slot="header">
${this.hass.localize("ui.panel.config.script.picker.header")}
</div>
<div slot="introduction"> <div slot="introduction">
The script editor allows you to create and edit scripts. Please read ${this.hass.localize("ui.panel.config.script.picker.introduction")}
<a <p>
href="https://home-assistant.io/docs/scripts/editor/" <a
target="_blank" href="https://home-assistant.io/docs/scripts/editor/"
>the instructions</a target="_blank"
> >
to make sure that you have configured Home Assistant correctly. ${this.hass.localize(
"ui.panel.config.script.picker.learn_more"
)}
</a>
</p>
</div> </div>
<ha-card header="Pick script to edit"> <ha-card>
${this.scripts.length === 0 ${this.scripts.length === 0
? html` ? html`
<div class="card-content"> <div class="card-content">
<p>We couldn't find any scripts.</p> <p>
${this.hass.localize(
"ui.panel.config.script.picker.no_scripts"
)}
</p>
</div> </div>
` `
: this.scripts.map( : this.scripts.map(
@ -85,7 +95,9 @@ class HaScriptPicker extends LitElement {
slot="fab" slot="fab"
?is-wide=${this.isWide} ?is-wide=${this.isWide}
icon="hass:plus" icon="hass:plus"
title="Add Script" title="${this.hass.localize(
"ui.panel.config.script.picker.add_script"
)}"
?rtl=${computeRTL(this.hass)} ?rtl=${computeRTL(this.hass)}
></ha-fab> ></ha-fab>
</a> </a>
@ -97,7 +109,11 @@ class HaScriptPicker extends LitElement {
const script = ev.currentTarget.script as HassEntity; const script = ev.currentTarget.script as HassEntity;
await triggerScript(this.hass, script.entity_id); await triggerScript(this.hass, script.entity_id);
showToast(this, { showToast(this, {
message: `Triggered ${computeStateName(script)}`, message: this.hass.localize(
"ui.notification_toast.triggered",
"name",
computeStateName(script)
),
}); });
} }

View File

@ -68,7 +68,8 @@ class HaUserPicker extends EventsMixin(
<div secondary=""> <div secondary="">
[[_computeGroup(localize, user)]] [[_computeGroup(localize, user)]]
<template is="dom-if" if="[[user.system_generated]]"> <template is="dom-if" if="[[user.system_generated]]">
- System Generated -
[[localize('ui.panel.config.users.picker.system_generated')]]
</template> </template>
</div> </div>
</paper-item-body> </paper-item-body>

View File

@ -52,15 +52,15 @@ class HaUserEditor extends LitElement {
<ha-card .header=${this._name}> <ha-card .header=${this._name}>
<table class="card-content"> <table class="card-content">
<tr> <tr>
<td>ID</td> <td>${hass.localize("ui.panel.config.users.editor.id")}</td>
<td>${user.id}</td> <td>${user.id}</td>
</tr> </tr>
<tr> <tr>
<td>Owner</td> <td>${hass.localize("ui.panel.config.users.editor.owner")}</td>
<td>${user.is_owner}</td> <td>${user.is_owner}</td>
</tr> </tr>
<tr> <tr>
<td>Group</td> <td>${hass.localize("ui.panel.config.users.editor.group")}</td>
<td> <td>
<select <select
@change=${this._handleGroupChange} @change=${this._handleGroupChange}
@ -92,11 +92,15 @@ class HaUserEditor extends LitElement {
: ""} : ""}
<tr> <tr>
<td>Active</td> <td>${hass.localize("ui.panel.config.users.editor.active")}</td>
<td>${user.is_active}</td> <td>${user.is_active}</td>
</tr> </tr>
<tr> <tr>
<td>System generated</td> <td>
${hass.localize(
"ui.panel.config.users.editor.system_generated"
)}
</td>
<td>${user.system_generated}</td> <td>${user.system_generated}</td>
</tr> </tr>
</table> </table>
@ -114,7 +118,9 @@ class HaUserEditor extends LitElement {
</mwc-button> </mwc-button>
${user.system_generated ${user.system_generated
? html` ? html`
Unable to remove system generated users. ${hass.localize(
"ui.panel.config.users.editor.system_generated_users_not_removable"
)}
` `
: ""} : ""}
</div> </div>
@ -124,12 +130,19 @@ class HaUserEditor extends LitElement {
} }
private get _name() { private get _name() {
return this.user && (this.user.name || "Unnamed user"); return (
this.user &&
(this.user.name ||
this.hass!.localize("ui.panel.config.users.editor.unnamed_user"))
);
} }
private async _handleRenameUser(ev): Promise<void> { private async _handleRenameUser(ev): Promise<void> {
ev.currentTarget.blur(); ev.currentTarget.blur();
const newName = prompt("New name?", this.user!.name); const newName = prompt(
this.hass!.localize("ui.panel.config.users.editor.enter_new_name"),
this.user!.name
);
if (newName === null || newName === this.user!.name) { if (newName === null || newName === this.user!.name) {
return; return;
} }
@ -140,7 +153,11 @@ class HaUserEditor extends LitElement {
}); });
fireEvent(this, "reload-users"); fireEvent(this, "reload-users");
} catch (err) { } catch (err) {
alert(`User rename failed: ${err.message}`); alert(
`${this.hass!.localize(
"ui.panel.config.users.editor.user_rename_failed"
)} ${err.message}`
);
} }
} }
@ -154,13 +171,25 @@ class HaUserEditor extends LitElement {
showSaveSuccessToast(this, this.hass!); showSaveSuccessToast(this, this.hass!);
fireEvent(this, "reload-users"); fireEvent(this, "reload-users");
} catch (err) { } catch (err) {
alert(`Group update failed: ${err.message}`); alert(
`${this.hass!.localize(
"ui.panel.config.users.editor.group_update_failed"
)} ${err.message}`
);
selectEl.value = this.user!.group_ids[0]; selectEl.value = this.user!.group_ids[0];
} }
} }
private async _deleteUser(ev): Promise<void> { private async _deleteUser(ev): Promise<void> {
if (!confirm(`Are you sure you want to delete ${this._name}`)) { if (
!confirm(
this.hass!.localize(
"ui.panel.config.users.editor.confirm_user_deletion",
"name",
this._name
)
)
) {
ev.target.blur(); ev.target.blur();
return; return;
} }

View File

@ -72,7 +72,9 @@ class ZHAAddDevicesPage extends LitElement {
: html` : html`
<div class="card-actions"> <div class="card-actions">
<mwc-button @click=${this._subscribe} class="search-button"> <mwc-button @click=${this._subscribe} class="search-button">
Search again ${this.hass!.localize(
"ui.panel.config.zha.add_device_page.search_again"
)}
</mwc-button> </mwc-button>
<paper-icon-button <paper-icon-button
class="toggle-help-icon" class="toggle-help-icon"

View File

@ -85,7 +85,11 @@ export class ZHAClusterAttributes extends LitElement {
return html` return html`
<ha-config-section .isWide="${this.isWide}"> <ha-config-section .isWide="${this.isWide}">
<div style="position: relative" slot="header"> <div style="position: relative" slot="header">
<span>Cluster Attributes</span> <span>
${this.hass!.localize(
"ui.panel.config.zha.cluster_attributes.header"
)}
</span>
<paper-icon-button <paper-icon-button
class="toggle-help-icon" class="toggle-help-icon"
@click="${this._onHelpTap}" @click="${this._onHelpTap}"
@ -93,12 +97,18 @@ export class ZHAClusterAttributes extends LitElement {
> >
</paper-icon-button> </paper-icon-button>
</div> </div>
<span slot="introduction">View and edit cluster attributes.</span> <span slot="introduction">
${this.hass!.localize(
"ui.panel.config.zha.cluster_attributes.introduction"
)}
</span>
<ha-card class="content"> <ha-card class="content">
<div class="attribute-picker"> <div class="attribute-picker">
<paper-dropdown-menu <paper-dropdown-menu
label="Attributes of the selected cluster" label="${this.hass!.localize(
"ui.panel.config.zha.cluster_attributes.attributes_of_cluster"
)}"
class="flex" class="flex"
> >
<paper-listbox <paper-listbox
@ -122,7 +132,9 @@ export class ZHAClusterAttributes extends LitElement {
${this.showHelp ${this.showHelp
? html` ? html`
<div class="help-text"> <div class="help-text">
Select an attribute to view or set its value ${this.hass!.localize(
"ui.panel.config.zha.cluster_attributes.help_attribute_dropdown"
)}
</div> </div>
` `
: ""} : ""}
@ -138,30 +150,40 @@ export class ZHAClusterAttributes extends LitElement {
return html` return html`
<div class="input-text"> <div class="input-text">
<paper-input <paper-input
label="Value" label="${this.hass!.localize("ui.panel.config.zha.common.value")}"
type="string" type="string"
.value="${this._attributeValue}" .value="${this._attributeValue}"
@value-changed="${this._onAttributeValueChanged}" @value-changed="${this._onAttributeValueChanged}"
placeholder="Value" placeholder="${this.hass!.localize(
"ui.panel.config.zha.common.value"
)}"
></paper-input> ></paper-input>
</div> </div>
<div class="input-text"> <div class="input-text">
<paper-input <paper-input
label="Manufacturer code override" label="${this.hass!.localize(
"ui.panel.config.zha.common.manufacturer_code_override"
)}"
type="number" type="number"
.value="${this._manufacturerCodeOverride}" .value="${this._manufacturerCodeOverride}"
@value-changed="${this._onManufacturerCodeOverrideChanged}" @value-changed="${this._onManufacturerCodeOverrideChanged}"
placeholder="Value" placeholder="${this.hass!.localize(
"ui.panel.config.zha.common.value"
)}"
></paper-input> ></paper-input>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<mwc-button @click="${this._onGetZigbeeAttributeClick}" <mwc-button @click="${this._onGetZigbeeAttributeClick}">
>Get Zigbee Attribute</mwc-button ${this.hass!.localize(
> "ui.panel.config.zha.cluster_attributes.get_zigbee_attribute"
)}
</mwc-button>
${this.showHelp ${this.showHelp
? html` ? html`
<div class="help-text2"> <div class="help-text2">
Get the value for the selected attribute ${this.hass!.localize(
"ui.panel.config.zha.cluster_attributes.help_get_zigbee_attribute"
)}
</div> </div>
` `
: ""} : ""}
@ -170,8 +192,11 @@ export class ZHAClusterAttributes extends LitElement {
domain="zha" domain="zha"
service="set_zigbee_cluster_attribute" service="set_zigbee_cluster_attribute"
.serviceData="${this._setAttributeServiceData}" .serviceData="${this._setAttributeServiceData}"
>Set Zigbee Attribute</ha-call-service-button
> >
${this.hass!.localize(
"ui.panel.config.zha.cluster_attributes.set_zigbee_attribute"
)}
</ha-call-service-button>
${this.showHelp ${this.showHelp
? html` ? html`
<ha-service-description <ha-service-description

View File

@ -78,7 +78,11 @@ export class ZHAClusterCommands extends LitElement {
return html` return html`
<ha-config-section .isWide="${this.isWide}"> <ha-config-section .isWide="${this.isWide}">
<div class="sectionHeader" slot="header"> <div class="sectionHeader" slot="header">
<span>Cluster Commands</span> <span>
${this.hass!.localize(
"ui.panel.config.zha.cluster_commands.header"
)}
</span>
<paper-icon-button <paper-icon-button
class="toggle-help-icon" class="toggle-help-icon"
@click="${this._onHelpTap}" @click="${this._onHelpTap}"
@ -86,12 +90,18 @@ export class ZHAClusterCommands extends LitElement {
> >
</paper-icon-button> </paper-icon-button>
</div> </div>
<span slot="introduction">View and issue cluster commands.</span> <span slot="introduction">
${this.hass!.localize(
"ui.panel.config.zha.cluster_commands.introduction"
)}
</span>
<ha-card class="content"> <ha-card class="content">
<div class="command-picker"> <div class="command-picker">
<paper-dropdown-menu <paper-dropdown-menu
label="Commands of the selected cluster" label="${this.hass!.localize(
"ui.panel.config.zha.cluster_commands.commands_of_cluster"
)}"
class="flex" class="flex"
> >
<paper-listbox <paper-listbox
@ -114,18 +124,26 @@ export class ZHAClusterCommands extends LitElement {
</div> </div>
${this._showHelp ${this._showHelp
? html` ? html`
<div class="help-text">Select a command to interact with</div> <div class="help-text">
${this.hass!.localize(
"ui.panel.config.zha.cluster_commands.help_command_dropdown"
)}
</div>
` `
: ""} : ""}
${this._selectedCommandIndex !== -1 ${this._selectedCommandIndex !== -1
? html` ? html`
<div class="input-text"> <div class="input-text">
<paper-input <paper-input
label="Manufacturer code override" label="${this.hass!.localize(
"ui.panel.config.zha.common.manufacturer_code_override"
)}"
type="number" type="number"
.value="${this._manufacturerCodeOverride}" .value="${this._manufacturerCodeOverride}"
@value-changed="${this._onManufacturerCodeOverrideChanged}" @value-changed="${this._onManufacturerCodeOverrideChanged}"
placeholder="Value" placeholder="${this.hass!.localize(
"ui.panel.config.zha.common.value"
)}"
></paper-input> ></paper-input>
</div> </div>
<div class="card-actions"> <div class="card-actions">
@ -134,8 +152,11 @@ export class ZHAClusterCommands extends LitElement {
domain="zha" domain="zha"
service="issue_zigbee_cluster_command" service="issue_zigbee_cluster_command"
.serviceData="${this._issueClusterCommandServiceData}" .serviceData="${this._issueClusterCommandServiceData}"
>Issue Zigbee Command</ha-call-service-button
> >
${this.hass!.localize(
"ui.panel.config.zha.cluster_commands.issue_zigbee_command"
)}
</ha-call-service-button>
${this._showHelp ${this._showHelp
? html` ? html`
<ha-service-description <ha-service-description

View File

@ -79,7 +79,10 @@ export class ZHAClusters extends LitElement {
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
return html` return html`
<div class="node-picker"> <div class="node-picker">
<paper-dropdown-menu label="Clusters" class="flex"> <paper-dropdown-menu
label="${this.hass!.localize("ui.panel.config.zha.common.clusters")}"
class="flex"
>
<paper-listbox <paper-listbox
slot="dropdown-content" slot="dropdown-content"
.selected="${this._selectedClusterIndex}" .selected="${this._selectedClusterIndex}"
@ -96,7 +99,9 @@ export class ZHAClusters extends LitElement {
${this.showHelp ${this.showHelp
? html` ? html`
<div class="help-text"> <div class="help-text">
Select cluster to view attributes and commands ${this.hass!.localize(
"ui.panel.config.zha.clusters.help_cluster_dropdown"
)}
</div> </div>
` `
: ""} : ""}

View File

@ -155,17 +155,29 @@ class ZHADeviceCard extends LitElement {
<dt>Nwk:</dt> <dt>Nwk:</dt>
<dd class="zha-info">${formatAsPaddedHex(this.device!.nwk)}</dd> <dd class="zha-info">${formatAsPaddedHex(this.device!.nwk)}</dd>
<dt>LQI:</dt> <dt>LQI:</dt>
<dd class="zha-info">${this.device!.lqi || "Unknown"}</dd> <dd class="zha-info">${this.device!.lqi ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}</dd>
<dt>RSSI:</dt> <dt>RSSI:</dt>
<dd class="zha-info">${this.device!.rssi || "Unknown"}</dd> <dd class="zha-info">${this.device!.rssi ||
<dt>Last Seen:</dt> this.hass!.localize("ui.dialogs.zha_device_info.unknown")}</dd>
<dd class="zha-info">${this.device!.last_seen || "Unknown"}</dd> <dt>${this.hass!.localize(
<dt>Power Source:</dt> "ui.dialogs.zha_device_info.last_seen"
<dd class="zha-info">${this.device!.power_source || "Unknown"}</dd> )}:</dt>
<dd class="zha-info">${this.device!.last_seen ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}</dd>
<dt>${this.hass!.localize(
"ui.dialogs.zha_device_info.power_source"
)}:</dt>
<dd class="zha-info">${this.device!.power_source ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}</dd>
${ ${
this.device!.quirk_applied this.device!.quirk_applied
? html` ? html`
<dt>Quirk:</dt> <dt>
${this.hass!.localize(
"ui.dialogs.zha_device_info.quirk"
)}:
</dt>
<dd class="zha-info">${this.device!.quirk_class}</dd> <dd class="zha-info">${this.device!.quirk_class}</dd>
` `
: "" : ""
@ -238,9 +250,11 @@ class ZHADeviceCard extends LitElement {
this.showActions this.showActions
? html` ? html`
<div class="card-actions"> <div class="card-actions">
<mwc-button @click="${this._onReconfigureNodeClick}" <mwc-button @click="${this._onReconfigureNodeClick}">
>Reconfigure Device</mwc-button ${this.hass!.localize(
> "ui.dialogs.zha_device_info.buttons.reconfigure"
)}
</mwc-button>
${this.showHelp ${this.showHelp
? html` ? html`
<div class="help-text"> <div class="help-text">
@ -256,8 +270,11 @@ class ZHADeviceCard extends LitElement {
domain="zha" domain="zha"
service="remove" service="remove"
.serviceData="${this._serviceData}" .serviceData="${this._serviceData}"
>Remove Device</ha-call-service-button
> >
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.remove"
)}
</ha-call-service-button>
${this.showHelp ${this.showHelp
? html` ? html`
<div class="help-text"> <div class="help-text">
@ -270,7 +287,9 @@ class ZHADeviceCard extends LitElement {
${this.device!.power_source === "Mains" ${this.device!.power_source === "Mains"
? html` ? html`
<mwc-button @click=${this._onAddDevicesClick}> <mwc-button @click=${this._onAddDevicesClick}>
Add Devices ${this.hass!.localize(
"ui.panel.config.zha.common.add_devices"
)}
</mwc-button> </mwc-button>
${this.showHelp ${this.showHelp
? html` ? html`

View File

@ -41,19 +41,27 @@ export class ZHANetwork extends LitElement {
return html` return html`
<ha-config-section .isWide="${this.isWide}"> <ha-config-section .isWide="${this.isWide}">
<div style="position: relative" slot="header"> <div style="position: relative" slot="header">
<span>Network Management</span> <span>
${this.hass!.localize(
"ui.panel.config.zha.network_management.header"
)}
</span>
<paper-icon-button <paper-icon-button
class="toggle-help-icon" class="toggle-help-icon"
@click="${this._onHelpTap}" @click="${this._onHelpTap}"
icon="hass:help-circle" icon="hass:help-circle"
></paper-icon-button> ></paper-icon-button>
</div> </div>
<span slot="introduction">Commands that affect entire network</span> <span slot="introduction">
${this.hass!.localize(
"ui.panel.config.zha.network_management.introduction"
)}
</span>
<ha-card class="content"> <ha-card class="content">
<div class="card-actions"> <div class="card-actions">
<mwc-button @click=${this._onAddDevicesClick}> <mwc-button @click=${this._onAddDevicesClick}>
Add Devices ${this.hass!.localize("ui.panel.config.zha.common.add_devices")}
</mwc-button> </mwc-button>
${this._showHelp ${this._showHelp
? html` ? html`

View File

@ -54,7 +54,11 @@ export class ZHANode extends LitElement {
return html` return html`
<ha-config-section .isWide="${this.isWide}"> <ha-config-section .isWide="${this.isWide}">
<div class="sectionHeader" slot="header"> <div class="sectionHeader" slot="header">
<span>Device Management</span> <span
>${this.hass!.localize(
"ui.panel.config.zha.node_management.header"
)}</span
>
<paper-icon-button <paper-icon-button
class="toggle-help-icon" class="toggle-help-icon"
@click="${this._onHelpTap}" @click="${this._onHelpTap}"
@ -62,18 +66,24 @@ export class ZHANode extends LitElement {
></paper-icon-button> ></paper-icon-button>
</div> </div>
<span slot="introduction"> <span slot="introduction">
Run ZHA commands that affect a single device. Pick a device to see a ${this.hass!.localize(
list of available commands. <br /><br />Note: Sleepy (battery powered) "ui.panel.config.zha.node_management.introduction"
devices need to be awake when executing commands against them. You can )}
generally wake a sleepy device by triggering it. <br /><br />Some <br /><br />
devices such as Xiaomi sensors have a wake up button that you can ${this.hass!.localize(
press at ~5 second intervals that keep devices awake while you "ui.panel.config.zha.node_management.hint_battery_devices"
interact with them. )}
<br /><br />
${this.hass!.localize(
"ui.panel.config.zha.node_management.hint_wakeup"
)}
</span> </span>
<ha-card class="content"> <ha-card class="content">
<div class="node-picker"> <div class="node-picker">
<paper-dropdown-menu <paper-dropdown-menu
label="Devices" label="${this.hass!.localize(
"ui.panel.config.zha.common.devices"
)}"
class="flex" class="flex"
id="zha-device-selector" id="zha-device-selector"
> >
@ -97,7 +107,9 @@ export class ZHANode extends LitElement {
${this._showHelp ${this._showHelp
? html` ? html`
<div class="help-text"> <div class="help-text">
Select device to view per-device options ${this.hass!.localize(
"ui.panel.config.zha.node_management.help_node_dropdown"
)}
</div> </div>
` `
: ""} : ""}

View File

@ -44,6 +44,11 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) {
margin-top: 24px; margin-top: 24px;
} }
.sectionHeader {
position: relative;
padding-right: 40px;
}
.node-info { .node-info {
margin-left: 16px; margin-left: 16px;
} }
@ -77,7 +82,7 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) {
.toggle-help-icon { .toggle-help-icon {
position: absolute; position: absolute;
top: 6px; top: -6px;
right: 0; right: 0;
color: var(--primary-color); color: var(--primary-color);
} }
@ -102,7 +107,7 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) {
<!-- Node card --> <!-- Node card -->
<ha-config-section is-wide="[[isWide]]"> <ha-config-section is-wide="[[isWide]]">
<div style="position: relative" slot="header"> <div class="sectionHeader" slot="header">
<span>Z-Wave Node Management</span> <span>Z-Wave Node Management</span>
<paper-icon-button <paper-icon-button
class="toggle-help-icon" class="toggle-help-icon"

12
src/panels/config/zwave/zwave-log.js Normal file → Executable file
View File

@ -3,6 +3,7 @@ import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import LocalizeMixin from "../../../mixins/localize-mixin";
import { EventsMixin } from "../../../mixins/events-mixin"; import { EventsMixin } from "../../../mixins/events-mixin";
import isPwa from "../../../common/config/is_pwa"; import isPwa from "../../../common/config/is_pwa";
@ -11,7 +12,7 @@ import "../../../components/ha-card";
let registeredDialog = false; let registeredDialog = false;
class OzwLog extends EventsMixin(PolymerElement) { class OzwLog extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() { static get template() {
return html` return html`
<style include="iron-flex ha-style"> <style include="iron-flex ha-style">
@ -32,8 +33,13 @@ class OzwLog extends EventsMixin(PolymerElement) {
</style> </style>
<ha-config-section is-wide="[[isWide]]"> <ha-config-section is-wide="[[isWide]]">
<span slot="header">OZW Log</span> <span slot="header">
<ha-card> [[localize('ui.panel.config.zwave.ozw_log.header')]]
</span>
<span slot="introduction">
[[localize('ui.panel.config.zwave.ozw_log.introduction')]]
</span>
<ha-card class="content">
<div class="device-picker"> <div class="device-picker">
<paper-input label="Number of last log lines." type="number" min="0" max="1000" step="10" value="{{numLogLines}}"> <paper-input label="Number of last log lines." type="number" min="0" max="1000" step="10" value="{{numLogLines}}">
</paper-input> </paper-input>

View File

@ -51,7 +51,7 @@ export class ZwaveNetwork extends LitElement {
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
return html` return html`
<ha-config-section .isWide="${this.isWide}"> <ha-config-section .isWide="${this.isWide}">
<div style="position: relative" slot="header"> <div class="sectionHeader" slot="header">
<span> <span>
${this.hass!.localize( ${this.hass!.localize(
"ui.panel.config.zwave.network_management.header" "ui.panel.config.zwave.network_management.header"
@ -63,11 +63,19 @@ export class ZwaveNetwork extends LitElement {
icon="hass:help-circle" icon="hass:help-circle"
></paper-icon-button> ></paper-icon-button>
</div> </div>
<span slot="introduction"> <div slot="introduction">
${this.hass!.localize( ${this.hass!.localize(
"ui.panel.config.zwave.network_management.introduction" "ui.panel.config.zwave.network_management.introduction"
)} )}
</span> <p>
<a
href="https://www.home-assistant.io/docs/z-wave/control-panel/"
target="_blank"
>
${this.hass!.localize("ui.panel.config.zwave.learn_more")}
</a>
</p>
</div>
${this._networkStatus ${this._networkStatus
? html` ? html`
@ -234,6 +242,11 @@ export class ZwaveNetwork extends LitElement {
margin-top: 24px; margin-top: 24px;
} }
.sectionHeader {
position: relative;
padding-right: 40px;
}
.network-status { .network-status {
text-align: center; text-align: center;
} }

View File

@ -1,17 +1,18 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes"; import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-input/paper-textarea";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import yaml from "js-yaml"; import yaml from "js-yaml";
import "../../../components/ha-code-editor";
import "../../../resources/ha-style"; import "../../../resources/ha-style";
import "./events-list"; import "./events-list";
import "./event-subscribe-card"; import "./event-subscribe-card";
import { EventsMixin } from "../../../mixins/events-mixin"; import { EventsMixin } from "../../../mixins/events-mixin";
const ERROR_SENTINEL = {};
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin
*/ */
@ -32,6 +33,11 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) {
.ha-form { .ha-form {
margin-right: 16px; margin-right: 16px;
max-width: 400px;
}
mwc-button {
margin-top: 8px;
} }
.header { .header {
@ -62,11 +68,16 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) {
required required
value="{{eventType}}" value="{{eventType}}"
></paper-input> ></paper-input>
<paper-textarea <p>Event Data (YAML, optional)</p>
label="Event Data (YAML, optional)" <ha-code-editor
value="{{eventData}}" mode="yaml"
></paper-textarea> value="[[eventData]]"
<mwc-button on-click="fireEvent" raised>Fire Event</mwc-button> error="[[!validJSON]]"
on-value-changed="_yamlChanged"
></ha-code-editor>
<mwc-button on-click="fireEvent" raised disabled="[[!validJSON]]"
>Fire Event</mwc-button
>
</div> </div>
</div> </div>
@ -97,6 +108,16 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) {
type: String, type: String,
value: "", value: "",
}, },
parsedJSON: {
type: Object,
computed: "_computeParsedEventData(eventData)",
},
validJSON: {
type: Boolean,
computed: "_computeValidJSON(parsedJSON)",
},
}; };
} }
@ -104,19 +125,28 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) {
this.eventType = ev.detail.eventType; this.eventType = ev.detail.eventType;
} }
fireEvent() { _computeParsedEventData(eventData) {
var eventData;
try { try {
eventData = this.eventData ? yaml.safeLoad(this.eventData) : {}; return eventData.trim() ? yaml.safeLoad(eventData) : {};
} catch (err) { } catch (err) {
/* eslint-disable no-alert */ return ERROR_SENTINEL;
alert("Error parsing YAML: " + err); }
/* eslint-enable no-alert */ }
_computeValidJSON(parsedJSON) {
return parsedJSON !== ERROR_SENTINEL;
}
_yamlChanged(ev) {
this.eventData = ev.detail.value;
}
fireEvent() {
if (!this.eventType) {
alert("Event type is a mandatory field");
return; return;
} }
this.hass.callApi("POST", "events/" + this.eventType, this.parsedJSON).then(
this.hass.callApi("POST", "events/" + this.eventType, eventData).then(
function() { function() {
this.fire("hass-notification", { this.fire("hass-notification", {
message: "Event " + this.eventType + " successful fired!", message: "Event " + this.eventType + " successful fired!",

View File

@ -110,9 +110,8 @@ class HaPanelDevInfo extends LitElement {
</p> </p>
<p> <p>
<a href="${nonDefaultLink}">${nonDefaultLinkText}</a><br /> <a href="${nonDefaultLink}">${nonDefaultLinkText}</a><br />
<mwc-button @click="${this._toggleDefaultPage}" raised> <a href="#" @click="${this._toggleDefaultPage}">${defaultPageText}</a
${defaultPageText} ><br />
</mwc-button>
</p> </p>
</div> </div>
<div class="content"> <div class="content">

View File

@ -9,12 +9,12 @@ import {
} from "lit-element"; } from "lit-element";
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-input/paper-textarea";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-code-editor";
import "./mqtt-subscribe-card"; import "./mqtt-subscribe-card";
@customElement("developer-tools-mqtt") @customElement("developer-tools-mqtt")
@ -48,12 +48,12 @@ class HaPanelDevMqtt extends LitElement {
@value-changed=${this._handleTopic} @value-changed=${this._handleTopic}
></paper-input> ></paper-input>
<paper-textarea <p>Payload (template allowed)</p>
always-float-label <ha-code-editor
label="Payload (template allowed)" mode="jinja2"
.value="${this.payload}" .value="${this.payload}"
@value-changed=${this._handlePayload} @value-changed=${this._handlePayload}
></paper-textarea> ></ha-code-editor>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<mwc-button @click=${this._publish}>Publish</mwc-button> <mwc-button @click=${this._publish}>Publish</mwc-button>

View File

@ -1,5 +1,4 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-input/paper-textarea";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
@ -7,6 +6,7 @@ import yaml from "js-yaml";
import { ENTITY_COMPONENT_DOMAINS } from "../../../data/entity"; import { ENTITY_COMPONENT_DOMAINS } from "../../../data/entity";
import "../../../components/entity/ha-entity-picker"; import "../../../components/entity/ha-entity-picker";
import "../../../components/ha-code-editor";
import "../../../components/ha-service-picker"; import "../../../components/ha-service-picker";
import "../../../resources/ha-style"; import "../../../resources/ha-style";
import "../../../util/app-localstorage-document"; import "../../../util/app-localstorage-document";
@ -30,6 +30,10 @@ class HaPanelDevService extends PolymerElement {
max-width: 400px; max-width: 400px;
} }
mwc-button {
margin-top: 8px;
}
.description { .description {
margin-top: 24px; margin-top: 24px;
white-space: pre-wrap; white-space: pre-wrap;
@ -109,20 +113,16 @@ class HaPanelDevService extends PolymerElement {
allow-custom-entity allow-custom-entity
></ha-entity-picker> ></ha-entity-picker>
</template> </template>
<paper-textarea <p>Service Data (YAML, optional)</p>
always-float-label <ha-code-editor
label="Service Data (YAML, optional)" mode="yaml"
value="{{serviceData}}" value="[[serviceData]]"
autocapitalize="none" error="[[!validJSON]]"
autocomplete="off" on-value-changed="_yamlChanged"
spellcheck="false" ></ha-code-editor>
></paper-textarea>
<mwc-button on-click="_callService" raised disabled="[[!validJSON]]"> <mwc-button on-click="_callService" raised disabled="[[!validJSON]]">
Call Service Call Service
</mwc-button> </mwc-button>
<template is="dom-if" if="[[!validJSON]]">
<span class="error">Invalid YAML</span>
</template>
</div> </div>
<template is="dom-if" if="[[!domainService]]"> <template is="dom-if" if="[[!domainService]]">
@ -305,6 +305,10 @@ class HaPanelDevService extends PolymerElement {
entity_id: ev.target.value, entity_id: ev.target.value,
}); });
} }
_yamlChanged(ev) {
this.serviceData = ev.detail.value;
}
} }
customElements.define("developer-tools-service", HaPanelDevService); customElements.define("developer-tools-service", HaPanelDevService);

View File

@ -1,16 +1,17 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-checkbox/paper-checkbox"; import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-input/paper-textarea";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import yaml from "js-yaml"; import yaml from "js-yaml";
import "../../../components/entity/ha-entity-picker"; import "../../../components/entity/ha-entity-picker";
import "../../../components/ha-code-editor";
import "../../../resources/ha-style"; import "../../../resources/ha-style";
import { EventsMixin } from "../../../mixins/events-mixin"; import { EventsMixin } from "../../../mixins/events-mixin";
const ERROR_SENTINEL = {};
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin
*/ */
@ -27,13 +28,14 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
direction: ltr; direction: ltr;
} }
ha-entity-picker, .inputs {
.state-input,
paper-textarea {
display: block;
max-width: 400px; max-width: 400px;
} }
mwc-button {
margin-top: 8px;
}
.entities th { .entities th {
text-align: left; text-align: left;
} }
@ -66,7 +68,7 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
} }
</style> </style>
<div> <div class="inputs">
<p> <p>
Set the representation of a device within Home Assistant.<br /> Set the representation of a device within Home Assistant.<br />
This will not communicate with the actual device. This will not communicate with the actual device.
@ -89,14 +91,16 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
value="{{_state}}" value="{{_state}}"
class="state-input" class="state-input"
></paper-input> ></paper-input>
<paper-textarea <p>State attributes (YAML, optional)</p>
label="State attributes (YAML, optional)" <ha-code-editor
autocapitalize="none" mode="yaml"
autocomplete="off" value="[[_stateAttributes]]"
spellcheck="false" error="[[!validJSON]]"
value="{{_stateAttributes}}" on-value-changed="_yamlChanged"
></paper-textarea> ></ha-code-editor>
<mwc-button on-click="handleSetState" raised>Set State</mwc-button> <mwc-button on-click="handleSetState" disabled="[[!validJSON]]" raised
>Set State</mwc-button
>
</div> </div>
<h1>Current entities</h1> <h1>Current entities</h1>
@ -166,6 +170,16 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
type: Object, type: Object,
}, },
parsedJSON: {
type: Object,
computed: "_computeParsedStateAttributes(_stateAttributes)",
},
validJSON: {
type: Boolean,
computed: "_computeValidJSON(parsedJSON)",
},
_entityId: { _entityId: {
type: String, type: String,
value: "", value: "",
@ -229,20 +243,13 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
} }
handleSetState() { handleSetState() {
var attr; if (!this._entityId) {
alert("Entity is a mandatory field");
try {
attr = this._stateAttributes ? yaml.safeLoad(this._stateAttributes) : {};
} catch (err) {
/* eslint-disable no-alert */
alert("Error parsing YAML: " + err);
/* eslint-enable no-alert */
return; return;
} }
this.hass.callApi("POST", "states/" + this._entityId, { this.hass.callApi("POST", "states/" + this._entityId, {
state: this._state, state: this._state,
attributes: attr, attributes: this.parsedJSON,
}); });
} }
@ -341,6 +348,22 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
return output; return output;
} }
_computeParsedStateAttributes(stateAttributes) {
try {
return stateAttributes.trim() ? yaml.safeLoad(stateAttributes) : {};
} catch (err) {
return ERROR_SENTINEL;
}
}
_computeValidJSON(parsedJSON) {
return parsedJSON !== ERROR_SENTINEL;
}
_yamlChanged(ev) {
this._stateAttributes = ev.detail.value;
}
} }
customElements.define("developer-tools-state", HaPanelDevState); customElements.define("developer-tools-state", HaPanelDevState);

View File

@ -1,9 +1,9 @@
import "@polymer/paper-input/paper-textarea";
import "@polymer/paper-spinner/paper-spinner"; import "@polymer/paper-spinner/paper-spinner";
import { timeOut } from "@polymer/polymer/lib/utils/async"; import { timeOut } from "@polymer/polymer/lib/utils/async";
import { Debouncer } from "@polymer/polymer/lib/utils/debounce"; import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-code-editor";
import "../../../resources/ha-style"; import "../../../resources/ha-style";
@ -46,12 +46,6 @@ class HaPanelDevTemplate extends PolymerElement {
right: 8px; right: 8px;
} }
paper-textarea {
--paper-input-container-input: {
@apply --paper-font-code1;
}
}
.rendered { .rendered {
@apply --paper-font-code1; @apply --paper-font-code1;
clear: both; clear: both;
@ -85,11 +79,14 @@ class HaPanelDevTemplate extends PolymerElement {
> >
</li> </li>
</ul> </ul>
<paper-textarea <p>Template editor</p>
label="Template editor" <ha-code-editor
value="{{template}}" mode="jinja2"
value="[[template]]"
error="[[error]]"
autofocus autofocus
></paper-textarea> on-value-changed="templateChanged"
></ha-code-editor>
</div> </div>
<div class="render-pane"> <div class="render-pane">
@ -144,7 +141,6 @@ For loop example:
{{ state.name | lower }} is {{state.state_with_unit}} {{ state.name | lower }} is {{state.state_with_unit}}
{%- endfor %}.`, {%- endfor %}.`,
/* eslint-enable max-len */ /* eslint-enable max-len */
observer: "templateChanged",
}, },
processed: { processed: {
@ -154,6 +150,11 @@ For loop example:
}; };
} }
ready() {
super.ready();
this.renderTemplate();
}
computeFormClasses(narrow) { computeFormClasses(narrow) {
return narrow ? "content fit" : "content fit layout horizontal"; return narrow ? "content fit" : "content fit layout horizontal";
} }
@ -162,7 +163,8 @@ For loop example:
return error ? "error rendered" : "rendered"; return error ? "error rendered" : "rendered";
} }
templateChanged() { templateChanged(ev) {
this.template = ev.detail.value;
if (this.error) { if (this.error) {
this.error = false; this.error = false;
} }

View File

@ -26,7 +26,10 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
} }
public static getStubConfig(): object { public static getStubConfig(): object {
return { content: " " }; return {
content:
"The **Markdown** card allows you to write any text. You can style it **bold**, *italicized*, ~strikethrough~ etc. You can do images, links, and more.\n\nFor more information see the [Markdown Cheatsheet](https://commonmark.org/help).",
};
} }
@property() private _config?: MarkdownCardConfig; @property() private _config?: MarkdownCardConfig;

View File

@ -1,4 +1,4 @@
import { html, LitElement, TemplateResult } from "lit-element"; import { html, LitElement, TemplateResult, CSSResult, css } from "lit-element";
import { createCardElement } from "../common/create-card-element"; import { createCardElement } from "../common/create-card-element";
import { LovelaceCard } from "../types"; import { LovelaceCard } from "../types";
@ -48,12 +48,31 @@ export abstract class HuiStackCard extends LitElement implements LovelaceCard {
return html` return html`
${this.renderStyle()} ${this.renderStyle()}
${this._config.title
? html`
<div class="card-header">${this._config.title}</div>
`
: ""}
<div id="root">${this._cards}</div> <div id="root">${this._cards}</div>
`; `;
} }
protected abstract renderStyle(): TemplateResult; protected abstract renderStyle(): TemplateResult;
static get styles(): CSSResult {
return css`
.card-header {
color: var(--ha-card-header-color, --primary-text-color);
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em;
line-height: 32px;
display: block;
padding: 24px 16px 16px;
}
`;
}
private _createCardElement(cardConfig: LovelaceCardConfig) { private _createCardElement(cardConfig: LovelaceCardConfig) {
const element = createCardElement(cardConfig) as LovelaceCard; const element = createCardElement(cardConfig) as LovelaceCard;
if (this._hass) { if (this._hass) {

2
src/panels/lovelace/common/compute-unused-entities.ts Normal file → Executable file
View File

@ -1,7 +1,7 @@
import { LovelaceConfig, ActionConfig } from "../../../data/lovelace"; import { LovelaceConfig, ActionConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
const EXCLUDED_DOMAINS = ["zone"]; const EXCLUDED_DOMAINS = ["zone", "persistent_notification"];
const addFromAction = (entities: Set<string>, actionConfig: ActionConfig) => { const addFromAction = (entities: Set<string>, actionConfig: ActionConfig) => {
if ( if (

View File

@ -1,137 +0,0 @@
// @ts-ignore
import CodeMirror from "codemirror";
import "codemirror/mode/yaml/yaml";
// @ts-ignore
import codeMirrorCSS from "codemirror/lib/codemirror.css";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeRTL } from "../../../common/util/compute_rtl";
import { customElement } from "lit-element";
declare global {
interface HASSDomEvents {
"yaml-changed": {
value: string;
};
"yaml-save": undefined;
}
}
@customElement("hui-yaml-editor")
export class HuiYamlEditor extends HTMLElement {
public _hass?: HomeAssistant;
public codemirror!: any;
private _value: string;
public constructor() {
super();
CodeMirror.commands.save = (cm: CodeMirror) => {
fireEvent(cm.getWrapperElement(), "yaml-save");
};
this._value = "";
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `
<style>
${codeMirrorCSS}
.CodeMirror {
height: var(--code-mirror-height, auto);
direction: var(--code-mirror-direction, ltr);
}
.CodeMirror-scroll {
max-height: var(--code-mirror-max-height, --code-mirror-height);
}
.CodeMirror-gutters {
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
transition: 0.2s ease border-right;
}
.CodeMirror-focused .CodeMirror-gutters {
border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));;
}
.CodeMirror-linenumber {
color: var(--paper-dialog-color, var(--primary-text-color));
}
.rtl .CodeMirror-vscrollbar {
right: auto;
left: 0px;
}
.rtl-gutter {
width: 20px;
}
</style>`;
}
set hass(hass: HomeAssistant) {
this._hass = hass;
if (this._hass) {
this.setScrollBarDirection();
}
}
set value(value: string) {
if (this.codemirror) {
if (value !== this.codemirror.getValue()) {
this.codemirror.setValue(value);
}
}
this._value = value;
}
get value(): string {
return this.codemirror.getValue();
}
get hasComments(): boolean {
return this.shadowRoot!.querySelector("span.cm-comment") ? true : false;
}
public connectedCallback(): void {
if (!this.codemirror) {
this.codemirror = CodeMirror(
(this.shadowRoot as unknown) as HTMLElement,
{
value: this._value,
lineNumbers: true,
mode: "yaml",
tabSize: 2,
autofocus: true,
viewportMargin: Infinity,
extraKeys: {
Tab: "indentMore",
"Shift-Tab": "indentLess",
},
gutters:
this._hass && computeRTL(this._hass!)
? ["rtl-gutter", "CodeMirror-linenumbers"]
: [],
}
);
this.setScrollBarDirection();
this.codemirror.on("changes", () => this._onChange());
} else {
this.codemirror.refresh();
}
}
private _onChange(): void {
fireEvent(this, "yaml-changed", { value: this.codemirror.getValue() });
}
private setScrollBarDirection(): void {
if (!this.codemirror) {
return;
}
this.codemirror
.getWrapperElement()
.classList.toggle("rtl", computeRTL(this._hass!));
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-yaml-editor": HuiYamlEditor;
}
}

View File

@ -15,11 +15,12 @@ import { HomeAssistant } from "../../../../types";
import { LovelaceCardConfig } from "../../../../data/lovelace"; import { LovelaceCardConfig } from "../../../../data/lovelace";
import { LovelaceCardEditor } from "../../types"; import { LovelaceCardEditor } from "../../types";
import { getCardElementTag } from "../../common/get-card-element-tag"; import { getCardElementTag } from "../../common/get-card-element-tag";
import { computeRTL } from "../../../../common/util/compute_rtl";
import "../../components/hui-yaml-editor"; import "../../../../components/ha-code-editor";
// This is not a duplicate import, one is for types, one is for element. // This is not a duplicate import, one is for types, one is for element.
// tslint:disable-next-line // tslint:disable-next-line
import { HuiYamlEditor } from "../../components/hui-yaml-editor"; import { HaCodeEditor } from "../../../../components/ha-code-editor";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { EntityConfig } from "../../entity-rows/types"; import { EntityConfig } from "../../entity-rows/types";
@ -43,7 +44,7 @@ export interface UIConfigChangedEvent extends Event {
@customElement("hui-card-editor") @customElement("hui-card-editor")
export class HuiCardEditor extends LitElement { export class HuiCardEditor extends LitElement {
@property() public hass?: HomeAssistant; @property() public hass!: HomeAssistant;
@property() private _yaml?: string; @property() private _yaml?: string;
@property() private _config?: LovelaceCardConfig; @property() private _config?: LovelaceCardConfig;
@ -64,12 +65,6 @@ export class HuiCardEditor extends LitElement {
try { try {
this._config = yaml.safeLoad(this.yaml); this._config = yaml.safeLoad(this.yaml);
this._updateConfigElement(); this._updateConfigElement();
setTimeout(() => {
if (this._yamlEditor) {
this._yamlEditor.codemirror.refresh();
}
fireEvent(this as HTMLElement, "iron-resize");
}, 1);
this._error = undefined; this._error = undefined;
} catch (err) { } catch (err) {
this._error = err.message; this._error = err.message;
@ -93,14 +88,19 @@ export class HuiCardEditor extends LitElement {
return this._error !== undefined; return this._error !== undefined;
} }
private get _yamlEditor(): HuiYamlEditor { private get _yamlEditor(): HaCodeEditor {
return this.shadowRoot!.querySelector("hui-yaml-editor")!; return this.shadowRoot!.querySelector("ha-code-editor")! as HaCodeEditor;
} }
public toggleMode() { public toggleMode() {
this._GUImode = !this._GUImode; this._GUImode = !this._GUImode;
} }
public connectedCallback() {
super.connectedCallback();
this._refreshYamlEditor();
}
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div class="wrapper"> <div class="wrapper">
@ -120,11 +120,14 @@ export class HuiCardEditor extends LitElement {
` `
: html` : html`
<div class="yaml-editor"> <div class="yaml-editor">
<hui-yaml-editor <ha-code-editor
.hass=${this.hass} mode="yaml"
autofocus
.value=${this.yaml} .value=${this.yaml}
@yaml-changed=${this._handleYAMLChanged} .error=${this._error}
></hui-yaml-editor> .rtl=${computeRTL(this.hass)}
@value-changed=${this._handleYAMLChanged}
></ha-code-editor>
</div> </div>
`} `}
${this._error ${this._error
@ -145,9 +148,12 @@ export class HuiCardEditor extends LitElement {
<mwc-button <mwc-button
@click=${this.toggleMode} @click=${this.toggleMode}
?disabled=${this._warning || this._error} ?disabled=${this._warning || this._error}
?unelevated=${this._GUImode === false}
> >
<ha-icon icon="mdi:code-braces"></ha-icon> ${this.hass!.localize(
this._GUImode
? "ui.panel.lovelace.editor.edit_card.show_code_editor"
: "ui.panel.lovelace.editor.edit_card.show_visual_editor"
)}
</mwc-button> </mwc-button>
</div> </div>
</div> </div>
@ -160,13 +166,25 @@ export class HuiCardEditor extends LitElement {
if (changedProperties.has("_GUImode")) { if (changedProperties.has("_GUImode")) {
if (this._GUImode === false) { if (this._GUImode === false) {
// Refresh code editor when switching to yaml mode // Refresh code editor when switching to yaml mode
this._yamlEditor.codemirror.refresh(); this._refreshYamlEditor(true);
this._yamlEditor.codemirror.focus();
} }
fireEvent(this as HTMLElement, "iron-resize"); fireEvent(this as HTMLElement, "iron-resize");
} }
} }
private _refreshYamlEditor(focus = false) {
// wait on render
setTimeout(() => {
if (this._yamlEditor && this._yamlEditor.codemirror) {
this._yamlEditor.codemirror.refresh();
if (focus) {
this._yamlEditor.codemirror.focus();
}
}
fireEvent(this as HTMLElement, "iron-resize");
}, 1);
}
private _handleUIConfigChanged(ev: UIConfigChangedEvent) { private _handleUIConfigChanged(ev: UIConfigChangedEvent) {
ev.stopPropagation(); ev.stopPropagation();
const config = ev.detail.config; const config = ev.detail.config;

View File

@ -14,31 +14,31 @@ import { getCardElementTag } from "../../common/get-card-element-tag";
import { CardPickTarget } from "../types"; import { CardPickTarget } from "../types";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
const cards = [ const cards: string[] = [
{ name: "Alarm panel", type: "alarm-panel" }, "alarm-panel",
{ name: "Conditional", type: "conditional" }, "conditional",
{ name: "Entities", type: "entities" }, "entities",
{ name: "Entity Button", type: "entity-button" }, "entity-button",
{ name: "Entity Filter", type: "entity-filter" }, "entity-filter",
{ name: "Gauge", type: "gauge" }, "gauge",
{ name: "Glance", type: "glance" }, "glance",
{ name: "History Graph", type: "history-graph" }, "history-graph",
{ name: "Horizontal Stack", type: "horizontal-stack" }, "horizontal-stack",
{ name: "iFrame", type: "iframe" }, "iframe",
{ name: "Light", type: "light" }, "light",
{ name: "Map", type: "map" }, "map",
{ name: "Markdown", type: "markdown" }, "markdown",
{ name: "Media Control", type: "media-control" }, "media-control",
{ name: "Picture", type: "picture" }, "picture",
{ name: "Picture Elements", type: "picture-elements" }, "picture-elements",
{ name: "Picture Entity", type: "picture-entity" }, "picture-entity",
{ name: "Picture Glance", type: "picture-glance" }, "picture-glance",
{ name: "Plant Status", type: "plant-status" }, "plant-status",
{ name: "Sensor", type: "sensor" }, "sensor",
{ name: "Shopping List", type: "shopping-list" }, "shopping-list",
{ name: "Thermostat", type: "thermostat" }, "thermostat",
{ name: "Vertical Stack", type: "vertical-stack" }, "vertical-stack",
{ name: "Weather Forecast", type: "weather-forecast" }, "weather-forecast",
]; ];
@customElement("hui-card-picker") @customElement("hui-card-picker")
@ -53,10 +53,12 @@ export class HuiCardPicker extends LitElement {
${this.hass!.localize("ui.panel.lovelace.editor.edit_card.pick_card")} ${this.hass!.localize("ui.panel.lovelace.editor.edit_card.pick_card")}
</h3> </h3>
<div class="cards-container"> <div class="cards-container">
${cards.map((card) => { ${cards.map((card: string) => {
return html` return html`
<mwc-button @click="${this._cardPicked}" .type="${card.type}"> <mwc-button @click="${this._cardPicked}" .type="${card}">
${card.name} ${this.hass!.localize(
`ui.panel.lovelace.editor.card.${card}.name`
)}
</mwc-button> </mwc-button>
`; `;
})} })}

View File

@ -62,10 +62,21 @@ export class HuiDialogEditCard extends LitElement {
return html``; return html``;
} }
let heading: string;
if (this._cardConfig && this._cardConfig.type) {
heading = `${this.hass!.localize(
`ui.panel.lovelace.editor.card.${this._cardConfig.type}.name`
)} ${this.hass!.localize("ui.panel.lovelace.editor.edit_card.header")}`;
} else {
heading = this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.header"
);
}
return html` return html`
<ha-paper-dialog with-backdrop opened modal> <ha-paper-dialog with-backdrop opened modal>
<h2> <h2>
${this.hass!.localize("ui.panel.lovelace.editor.edit_card.header")} ${heading}
</h2> </h2>
<paper-dialog-scrollable> <paper-dialog-scrollable>
${this._cardConfig === undefined ${this._cardConfig === undefined

View File

@ -101,7 +101,7 @@ export class HuiAlarmPanelCardEditor extends LitElement
})} })}
<paper-dropdown-menu <paper-dropdown-menu
.label="${this.hass.localize( .label="${this.hass.localize(
"ui.panel.lovelace.editor.card.alarm_panel.available_states" "ui.panel.lovelace.editor.card.alarm-panel.available_states"
)}" )}"
@value-changed="${this._stateAdded}" @value-changed="${this._stateAdded}"
> >

View File

@ -22,7 +22,7 @@ import "../../../../components/data-table/ha-data-table";
// tslint:disable-next-line // tslint:disable-next-line
import { import {
SelectionChangedEvent, SelectionChangedEvent,
DataTabelColumnContainer, DataTableColumnContainer,
} from "../../../../components/data-table/ha-data-table"; } from "../../../../components/data-table/ha-data-table";
import { computeStateName } from "../../../../common/entity/compute_state_name"; import { computeStateName } from "../../../../common/entity/compute_state_name";
@ -55,7 +55,7 @@ export class HuiUnusedEntities extends LitElement {
} }
private _columns = memoizeOne((narrow: boolean) => { private _columns = memoizeOne((narrow: boolean) => {
const columns: DataTabelColumnContainer = { const columns: DataTableColumnContainer = {
entity: { entity: {
title: "Entity", title: "Entity",
sortable: true, sortable: true,

View File

@ -66,7 +66,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements EntityRow {
<hui-generic-entity-row <hui-generic-entity-row
.hass="${this.hass}" .hass="${this.hass}"
.config="${this._config}" .config="${this._config}"
.showSecondary="false" .showSecondary=${false}
> >
${OFF_STATES.includes(stateObj.state) ${OFF_STATES.includes(stateObj.state)
? html` ? html`

View File

@ -14,11 +14,12 @@ import { Lovelace } from "./types";
import "../../components/ha-icon"; import "../../components/ha-icon";
import { haStyle } from "../../resources/styles"; import { haStyle } from "../../resources/styles";
import "./components/hui-yaml-editor"; import "../../components/ha-code-editor";
// This is not a duplicate import, one is for types, one is for element. // This is not a duplicate import, one is for types, one is for element.
// tslint:disable-next-line // tslint:disable-next-line
import { HuiYamlEditor } from "./components/hui-yaml-editor"; import { HaCodeEditor } from "../../components/ha-code-editor";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { computeRTL } from "../../common/util/compute_rtl";
const lovelaceStruct = struct.interface({ const lovelaceStruct = struct.interface({
title: "string?", title: "string?",
@ -27,12 +28,12 @@ const lovelaceStruct = struct.interface({
}); });
class LovelaceFullConfigEditor extends LitElement { class LovelaceFullConfigEditor extends LitElement {
public hass?: HomeAssistant; public hass!: HomeAssistant;
public lovelace?: Lovelace; public lovelace?: Lovelace;
public closeEditor?: () => void; public closeEditor?: () => void;
private _saving?: boolean; private _saving?: boolean;
private _changed?: boolean; private _changed?: boolean;
private _generation?: number; private _generation = 1;
static get properties() { static get properties() {
return { return {
@ -80,12 +81,15 @@ class LovelaceFullConfigEditor extends LitElement {
</app-toolbar> </app-toolbar>
</app-header> </app-header>
<div class="content"> <div class="content">
<hui-yaml-editor <ha-code-editor
mode="yaml"
autofocus
.rtl=${computeRTL(this.hass)}
.hass="${this.hass}" .hass="${this.hass}"
@yaml-changed="${this._yamlChanged}" @value-changed="${this._yamlChanged}"
@yaml-save="${this._handleSave}" @editor-save="${this._handleSave}"
> >
</hui-yaml-editor> </ha-code-editor>
</div> </div>
</app-header-layout> </app-header-layout>
`; `;
@ -93,8 +97,6 @@ class LovelaceFullConfigEditor extends LitElement {
protected firstUpdated() { protected firstUpdated() {
this.yamlEditor.value = yaml.safeDump(this.lovelace!.config); this.yamlEditor.value = yaml.safeDump(this.lovelace!.config);
this.yamlEditor.codemirror.clearHistory();
this._generation = this.yamlEditor.codemirror.changeGeneration(true);
} }
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
@ -140,10 +142,9 @@ class LovelaceFullConfigEditor extends LitElement {
} }
private _yamlChanged() { private _yamlChanged() {
if (!this._generation) { this._changed = !this.yamlEditor
return; .codemirror!.getDoc()
} .isClean(this._generation);
this._changed = !this.yamlEditor.codemirror.isClean(this._generation);
if (this._changed && !window.onbeforeunload) { if (this._changed && !window.onbeforeunload) {
window.onbeforeunload = () => { window.onbeforeunload = () => {
return true; return true;
@ -199,14 +200,16 @@ class LovelaceFullConfigEditor extends LitElement {
} catch (err) { } catch (err) {
alert(`Unable to save YAML: ${err}`); alert(`Unable to save YAML: ${err}`);
} }
this._generation = this.yamlEditor.codemirror.changeGeneration(true); this._generation = this.yamlEditor
.codemirror!.getDoc()
.changeGeneration(true);
window.onbeforeunload = null; window.onbeforeunload = null;
this._saving = false; this._saving = false;
this._changed = false; this._changed = false;
} }
private get yamlEditor(): HuiYamlEditor { private get yamlEditor(): HaCodeEditor {
return this.shadowRoot!.querySelector("hui-yaml-editor")!; return this.shadowRoot!.querySelector("ha-code-editor")! as HaCodeEditor;
} }
} }

View File

@ -236,7 +236,20 @@ class HUIRoot extends LitElement {
> >
${this.lovelace!.config.views.map( ${this.lovelace!.config.views.map(
(view) => html` (view) => html`
<paper-tab aria-label="${view.title}"> <paper-tab
aria-label="${view.title}"
class="${classMap({
"hide-tab": Boolean(
!this._editMode &&
view.visible !== undefined &&
((Array.isArray(view.visible) &&
!view.visible.some(
(e) => e.user === this.hass!.user!.id
)) ||
view.visible === false)
),
})}"
>
${this._editMode ${this._editMode
? html` ? html`
<ha-paper-icon-button-arrow-prev <ha-paper-icon-button-arrow-prev
@ -392,6 +405,9 @@ class HUIRoot extends LitElement {
paper-item { paper-item {
cursor: pointer; cursor: pointer;
} }
.hide-tab {
display: none;
}
`, `,
]; ];
} }

View File

@ -138,7 +138,6 @@ export class HUIView extends LitElement {
flex-basis: 0; flex-basis: 0;
flex-grow: 1; flex-grow: 1;
max-width: 500px; max-width: 500px;
overflow-x: hidden;
} }
.column > * { .column > * {

View File

@ -23,7 +23,9 @@ class AdvancedModeCard extends LitElement {
return html` return html`
<ha-card> <ha-card>
<div class="card-header"> <div class="card-header">
<div class="title">Advanced mode</div> <div class="title">
${this.hass.localize("ui.panel.profile.advanced_mode.title")}
</div>
<ha-switch <ha-switch
.checked=${this.coreUserData && this.coreUserData.showAdvanced} .checked=${this.coreUserData && this.coreUserData.showAdvanced}
.disabled=${this.coreUserData === undefined} .disabled=${this.coreUserData === undefined}
@ -31,10 +33,7 @@ class AdvancedModeCard extends LitElement {
></ha-switch> ></ha-switch>
</div> </div>
<div class="card-content"> <div class="card-content">
Home Assistant hides advanced features and options by default. You can ${this.hass.localize("ui.panel.profile.advanced_mode.description")}
make these features accessible by checking this toggle. This is a
user-specific setting and does not impact other users using Home
Assistant.
</div> </div>
</ha-card> </ha-card>
`; `;

View File

@ -28,7 +28,7 @@ class HaPushNotificationsRow extends LocalizeMixin(PolymerElement) {
<span slot="description"> <span slot="description">
[[_description(_platformLoaded, _pushSupported)]] [[_description(_platformLoaded, _pushSupported)]]
<a <a
href="https://www.home-assistant.io/integrations/notify.html5/" href="https://www.home-assistant.io/integrations/html5"
target="_blank" target="_blank"
>[[localize('ui.panel.profile.push_notifications.link_promo')]]</a >[[localize('ui.panel.profile.push_notifications.link_promo')]]</a
> >

View File

@ -0,0 +1,13 @@
interface LoadedCodeMirror {
codeMirror: any;
codeMirrorCss: any;
}
let loaded: Promise<LoadedCodeMirror>;
export const loadCodeMirror = async (): Promise<LoadedCodeMirror> => {
if (!loaded) {
loaded = import(/* webpackChunkName: "codemirror" */ "./codemirror");
}
return loaded;
};

View File

@ -0,0 +1,13 @@
// @ts-ignore
import _CodeMirror, { Editor } from "codemirror";
// @ts-ignore
import _codeMirrorCss from "codemirror/lib/codemirror.css";
import "codemirror/mode/yaml/yaml";
import "codemirror/mode/jinja2/jinja2";
import { fireEvent } from "../common/dom/fire_event";
_CodeMirror.commands.save = (cm: Editor) => {
fireEvent(cm.getWrapperElement(), "editor-save");
};
export const codeMirror: any = _CodeMirror;
export const codeMirrorCss: any = _codeMirrorCss;

View File

@ -33,6 +33,8 @@ documentContainer.innerHTML = `<custom-style>
--scrollbar-thumb-color: rgb(194, 194, 194); --scrollbar-thumb-color: rgb(194, 194, 194);
--error-state-color: #db4437;
/* states and badges */ /* states and badges */
--state-icon-color: #44739e; --state-icon-color: #44739e;
--state-icon-active-color: #FDD835; --state-icon-active-color: #FDD835;

352
src/translations/en.json Normal file → Executable file
View File

@ -566,11 +566,20 @@
"zha_device_info": { "zha_device_info": {
"manuf": "by {manufacturer}", "manuf": "by {manufacturer}",
"no_area": "No Area", "no_area": "No Area",
"buttons": {
"add": "Add Devices",
"remove": "Remove Device",
"reconfigure": "Reconfigure Device"
},
"services": { "services": {
"reconfigure": "Reconfigure ZHA device (heal device). Use this if you are having issues with the device. If the device in question is a battery powered device please ensure it is awake and accepting commands when you use this service.", "reconfigure": "Reconfigure ZHA device (heal device). Use this if you are having issues with the device. If the device in question is a battery powered device please ensure it is awake and accepting commands when you use this service.",
"updateDeviceName": "Set a custom name for this device in the device registry.", "updateDeviceName": "Set a custom name for this device in the device registry.",
"remove": "Remove a device from the ZigBee network." "remove": "Remove a device from the Zigbee network."
}, },
"quirk": "Quirk",
"last_seen": "Last Seen",
"power_source": "Power Source",
"unknown": "Unknown",
"zha_device_card": { "zha_device_card": {
"device_name_placeholder": "User given name", "device_name_placeholder": "User given name",
"area_picker_label": "Area", "area_picker_label": "Area",
@ -597,7 +606,8 @@
}, },
"notification_toast": { "notification_toast": {
"service_call_failed": "Failed to call service {service}.", "service_call_failed": "Failed to call service {service}.",
"connection_lost": "Connection lost. Reconnecting…" "connection_lost": "Connection lost. Reconnecting…",
"triggered": "Triggered {name}"
}, },
"sidebar": { "sidebar": {
"external_app_configuration": "App Configuration" "external_app_configuration": "App Configuration"
@ -606,6 +616,11 @@
"config": { "config": {
"header": "Configure Home Assistant", "header": "Configure Home Assistant",
"introduction": "Here it is possible to configure your components and Home Assistant. Not everything is possible to configure from the UI yet, but we're working on it.", "introduction": "Here it is possible to configure your components and Home Assistant. Not everything is possible to configure from the UI yet, but we're working on it.",
"common": {
"editor": {
"confirm_unsaved": "You have unsaved changes. Are you sure you want to leave?"
}
},
"area_registry": { "area_registry": {
"caption": "Area Registry", "caption": "Area Registry",
"description": "Overview of all areas in your home.", "description": "Overview of all areas in your home.",
@ -815,7 +830,12 @@
"label": "And" "label": "And"
}, },
"device": { "device": {
"label": "Device" "label": "Device",
"extra_fields": {
"above": "Above",
"below": "Below",
"for": "Duration"
}
}, },
"numeric_state": { "numeric_state": {
"label": "[%key:ui::panel::config::automation::editor::triggers::type::numeric_state::label%]", "label": "[%key:ui::panel::config::automation::editor::triggers::type::numeric_state::label%]",
@ -889,6 +909,9 @@
}, },
"device_id": { "device_id": {
"label": "Device" "label": "Device"
},
"scene": {
"label": "Activate scene"
} }
} }
} }
@ -896,13 +919,175 @@
}, },
"script": { "script": {
"caption": "Script", "caption": "Script",
"description": "Create and edit scripts" "description": "Create and edit scripts",
"picker": {
"header": "Script Editor",
"introduction": "The script editor allows you to create and edit scripts. Please follow the link below to read the instructions to make sure that you have configured Home Assistant correctly.",
"learn_more": "Learn more about scripts",
"no_scripts": "We couldnt find any editable scripts",
"add_script": "Add script"
},
"editor": {
"header": "Script: {name}",
"default_name": "New Script",
"load_error_not_editable": "Only scripts inside scripts.yaml are editable.",
"delete_confirm": "Are you sure you want to delete this script?"
}
}, },
"cloud": { "cloud": {
"caption": "Home Assistant Cloud", "caption": "Home Assistant Cloud",
"description_login": "Logged in as {email}", "description_login": "Logged in as {email}",
"description_not_login": "Not logged in", "description_not_login": "Not logged in",
"description_features": "Control away from home, integrate with Alexa and Google Assistant." "description_features": "Control away from home, integrate with Alexa and Google Assistant.",
"login": {
"title": "Cloud Login",
"introduction": "Home Assistant Cloud provides you with a secure remote connection to your instance while away from home. It also allows you to connect with cloud-only services: Amazon Alexa and Google Assistant.",
"introduction2": "This service is run by our partner ",
"introduction2a": ", a company founded by the founders of Home Assistant and Hass.io.",
"introduction3": "Home Assistant Cloud is a subscription service with a free one month trial. No payment information necessary.",
"learn_more_link": "Learn more about Home Assistant Cloud",
"dismiss": "Dismiss",
"sign_in": "Sign in",
"email": "Email",
"email_error_msg": "Invalid email",
"password": "Password",
"password_error_msg": "Passwords are at least 8 characters",
"forgot_password": "forgot password?",
"start_trial": "Start your free 1 month trial",
"trial_info": "No payment information necessary",
"alert_password_change_required": "You need to change your password before logging in.",
"alert_email_confirm_necessary": "You need to confirm your email before logging in."
},
"forgot_password": {
"title": "Forgot password",
"subtitle": "Forgot your password",
"instructions": "Enter your email address and we will send you a link to reset your password.",
"email": "Email",
"email_error_msg": "Invalid email",
"send_reset_email": "Send reset email",
"check_your_email": "Check your email for instructions on how to reset your password."
},
"register": {
"title": "Register Account",
"headline": "Start your free trial",
"information": "Create an account to start your free one month trial with Home Assistant Cloud. No payment information necessary.",
"information2": "The trial will give you access to all the benefits of Home Assistant Cloud, including:",
"feature_remote_control": "Control of Home Assistant away from home",
"feature_google_home": "Integration with Google Assistant",
"feature_amazon_alexa": "Integration with Amazon Alexa",
"feature_webhook_apps": "Easy integration with webhook-based apps like OwnTracks",
"information3": "This service is run by our partner ",
"information3a": ", a company founded by the founders of Home Assistant and Hass.io.",
"information4": "By registering an account you agree to the following terms and conditions.",
"link_terms_conditions": "Terms and Conditions",
"link_privacy_policy": "Privacy Policy",
"create_account": "Create Account",
"email_address": "Email address",
"email_error_msg": "Invalid email",
"password": "Password",
"password_error_msg": "Passwords are at least 8 characters",
"start_trial": "Start Trial",
"resend_confirm_email": "Resend confirmation email",
"account_created": "Account created! Check your email for instructions on how to activate your account."
},
"account": {
"thank_you_note": "Thank you for being part of Home Assistant Cloud. It's because of people like you that we are able to make a great home automation experience for everyone. Thank you!",
"nabu_casa_account": "Nabu Casa Account",
"connection_status": "Cloud connection status",
"manage_account": "Manage Account",
"sign_out": "Sign out",
"integrations": "Integrations",
"integrations_introduction": "Integrations for Home Assistant Cloud allow you to connect with services in the cloud without having to expose your Home Assistant instance publicly on the internet.",
"integrations_introduction2": "Check the website for ",
"integrations_link_all_features": " all available features",
"connected": "Connected",
"not_connected": "Not Connected",
"fetching_subscription": "Fetching subscription…",
"remote": {
"title": "Remote Control",
"access_is_being_prepared": "Remote access is being prepared. We will notify you when it's ready.",
"info": "Home Assistant Cloud provides a secure remote connection to your instance while away from home.",
"instance_is_available": "Your instance is available at",
"instance_will_be_available": "Your instance will be available at",
"link_learn_how_it_works": "Learn how it works",
"certificate_info": "Certificate Info"
},
"alexa": {
"title": "Alexa",
"info": "With the Alexa integration for Home Assistant Cloud you'll be able to control all your Home Assistant devices via any Alexa-enabled device.",
"enable_ha_skill": "Enable the Home Assistant skill for Alexa",
"config_documentation": "Config documentation",
"enable_state_reporting": "Enable State Reporting",
"info_state_reporting": "If you enable state reporting, Home Assistant will send all state changes of exposed entities to Amazon. This allows you to always see the latest states in the Alexa app and use the state changes to create routines.",
"sync_entities": "Sync Entities",
"manage_entities": "Manage Entities",
"sync_entities_error": "Failed to sync entities:",
"state_reporting_error": "Unable to {enable_disable} report state.",
"enable": "enable",
"disable": "disable"
},
"google": {
"title": "Google Assistant",
"info": "With the Google Assistant integration for Home Assistant Cloud you'll be able to control all your Home Assistant devices via any Google Assistant-enabled device.",
"enable_ha_skill": "Activate the Home Assistant skill for Google Assistant",
"config_documentation": "Config documentation",
"enable_state_reporting": "Enable State Reporting",
"info_state_reporting": "If you enable state reporting, Home Assistant will send all state changes of exposed entities to Google. This allows you to always see the latest states in the Google app.",
"security_devices": "Security Devices",
"enter_pin_info": "Please enter a pin to interact with security devices. Security devices are doors, garage doors and locks. You will be asked to say/enter this pin when interacting with such devices via Google Assistant.",
"devices_pin": "Security Devices Pin",
"enter_pin_hint": "Enter a PIN to use security devices",
"sync_entities": "Sync Entities to Google",
"manage_entities": "Manage Entities",
"enter_pin_error": "Unable to store pin:"
},
"webhooks": {
"title": "Webhooks",
"info": "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.",
"no_hooks_yet": "Looks like you have no webhooks yet. Get started by configuring a ",
"no_hooks_yet_link_integration": "webhook-based integration",
"no_hooks_yet2": " or by creating a ",
"no_hooks_yet_link_automation": "webhook automation",
"link_learn_more": "Learn more about creating webhook-powered automations.",
"loading": "Loading ...",
"manage": "Manage",
"disable_hook_error_msg": "Failed to disable webhook:"
}
},
"alexa": {
"title": "Alexa",
"banner": "Editing which entities are exposed via this UI is disabled because you have configured entity filters in configuration.yaml.",
"exposed_entities": "Exposed entities",
"not_exposed_entities": "Not Exposed entities",
"expose": "Expose to Alexa"
},
"dialog_certificate": {
"certificate_information": "Certificate Information",
"certificate_expiration_date": "Certificate expiration date",
"will_be_auto_renewed": "Will be automatically renewed",
"fingerprint": "Certificate fingerprint:",
"close": "Close"
},
"google": {
"title": "Google Assistant",
"expose": "Expose to Google Assistant",
"disable_2FA": "Disable two factor authentication",
"banner": "Editing which entities are exposed via this UI is disabled because you have configured entity filters in configuration.yaml.",
"exposed_entities": "Exposed entities",
"not_exposed_entities": "Not Exposed entities",
"sync_to_google": "Synchronizing changes to Google."
},
"dialog_cloudhook": {
"webhook_for": "Webhook for {name}",
"available_at": "The webhook is available at the following url:",
"managed_by_integration": "This webhook is managed by an integration and cannot be disabled.",
"info_disable_webhook": "If you no longer want to use this webhook, you can",
"link_disable_webhook": "disable it",
"view_documentation": "View documentation",
"close": "Close",
"confirm_disable": "Are you sure you want to disable this webhook?",
"copied_to_clipboard": "Copied to clipboard"
}
}, },
"devices": { "devices": {
"caption": "Devices", "caption": "Devices",
@ -927,7 +1112,13 @@
"introduction": "Home Assistant keeps a registry of every entity it has ever seen that can be uniquely identified. Each of these entities will have an entity ID assigned which will be reserved for just this entity.", "introduction": "Home Assistant keeps a registry of every entity it has ever seen that can be uniquely identified. Each of these entities will have an entity ID assigned which will be reserved for just this entity.",
"introduction2": "Use the entity registry to override the name, change the entity ID or remove the entry from Home Assistant. Note, removing the entity registry entry won't remove the entity. To do that, follow the link below and remove it from the integrations page.", "introduction2": "Use the entity registry to override the name, change the entity ID or remove the entry from Home Assistant. Note, removing the entity registry entry won't remove the entity. To do that, follow the link below and remove it from the integrations page.",
"integrations_page": "Integrations page", "integrations_page": "Integrations page",
"show_disabled": "Show disabled entities" "show_disabled": "Show disabled entities",
"headers": {
"name": "Name",
"entity_id": "Entity ID",
"integration": "Integration",
"enabled": "Enabled"
}
}, },
"editor": { "editor": {
"unavailable": "This entity is not currently available.", "unavailable": "This entity is not currently available.",
@ -944,11 +1135,27 @@
"person": { "person": {
"caption": "Persons", "caption": "Persons",
"description": "Manage the persons that Home Assistant tracks.", "description": "Manage the persons that Home Assistant tracks.",
"introduction": "Here you can define each person of interest in Home Assistant.",
"note_about_persons_configured_in_yaml": "Note: persons configured via configuration.yaml cannot be edited via the UI.",
"no_persons_created_yet": "Looks like you have not created any persons yet.",
"create_person": "Create Person",
"add_person": "Add Person",
"confirm_delete": "Are you sure you want to delete this person?",
"confirm_delete2": "All devices belonging to this person will become unassigned.",
"detail": { "detail": {
"new_person": "New Person",
"name": "Name", "name": "Name",
"name_error_msg": "Name is required",
"linked_user": "Linked User",
"device_tracker_intro": "Select the devices that belong to this person.", "device_tracker_intro": "Select the devices that belong to this person.",
"no_device_tracker_available_intro": "When you have devices that indicate the presence of a person, you will be able to assign them to a person here. You can add your first device by adding a presence-detection integration from the integrations page.",
"link_presence_detection_integrations": "Presence Detection Integrations",
"link_integrations_page": "Integrations page",
"device_tracker_picked": "Track Device", "device_tracker_picked": "Track Device",
"device_tracker_pick": "Pick device to track" "device_tracker_pick": "Pick device to track",
"delete": "Delete",
"create": "Create",
"update": "Update"
} }
}, },
"integrations": { "integrations": {
@ -957,6 +1164,9 @@
"discovered": "Discovered", "discovered": "Discovered",
"configured": "Configured", "configured": "Configured",
"new": "Set up a new integration", "new": "Set up a new integration",
"note_about_integrations": "Not all integrations can be configured via the UI yet.",
"note_about_website_reference": "More are available on the ",
"home_assistant_website": "Home Assistant website",
"configure": "Configure", "configure": "Configure",
"none": "Nothing configured yet", "none": "Nothing configured yet",
"config_entry": { "config_entry": {
@ -982,7 +1192,8 @@
"caption": "Users", "caption": "Users",
"description": "Manage users", "description": "Manage users",
"picker": { "picker": {
"title": "Users" "title": "Users",
"system_generated": "System generated"
}, },
"editor": { "editor": {
"caption": "View user", "caption": "View user",
@ -990,7 +1201,18 @@
"change_password": "Change password", "change_password": "Change password",
"activate_user": "Activate user", "activate_user": "Activate user",
"deactivate_user": "Deactivate user", "deactivate_user": "Deactivate user",
"delete_user": "Delete user" "delete_user": "Delete user",
"id": "ID",
"owner": "Owner",
"group": "Group",
"active": "Active",
"system_generated": "System generated",
"system_generated_users_not_removable": "Unable to remove system generated users.",
"unnamed_user": "Unnamed User",
"enter_new_name": "Enter new name",
"user_rename_failed": "User rename failed:",
"group_update_failed": "Group update failed:",
"confirm_user_deletion": "Are you sure you want to delete {name}?"
}, },
"add_user": { "add_user": {
"caption": "Add user", "caption": "Add user",
@ -1003,15 +1225,55 @@
"zha": { "zha": {
"caption": "ZHA", "caption": "ZHA",
"description": "Zigbee Home Automation network management", "description": "Zigbee Home Automation network management",
"common": {
"add_devices": "Add Devices",
"clusters": "Clusters",
"devices": "Devices",
"manufacturer_code_override": "Manufacturer Code Override",
"value": "Value"
},
"add_device_page": { "add_device_page": {
"header": "Zigbee Home Automation - Add Devices", "header": "Zigbee Home Automation - Add Devices",
"spinner": "Searching for ZHA Zigbee devices...", "spinner": "Searching for ZHA Zigbee devices...",
"discovery_text": "Discovered devices will show up here. Follow the instructions for your device(s) and place the device(s) in pairing mode." "discovery_text": "Discovered devices will show up here. Follow the instructions for your device(s) and place the device(s) in pairing mode.",
"search_again": "Search Again"
},
"network_management": {
"header": "Network Management",
"introduction": "Commands that affect the entire network"
},
"node_management": {
"header": "Device Management",
"introduction": "Run ZHA commands that affect a single device. Pick a device to see a list of available commands.",
"hint_battery_devices": "Note: Sleepy (battery powered) devices need to be awake when executing commands against them. You can generally wake a sleepy device by triggering it.",
"hint_wakeup": "Some devices such as Xiaomi sensors have a wake up button that you can press at ~5 second intervals that keep devices awake while you interact with them.",
"help_node_dropdown": "Select a device to view per-device options."
},
"clusters": {
"help_cluster_dropdown": "Select a cluster to view attributes and commands."
},
"cluster_attributes": {
"header": "Cluster Attributes",
"introduction": "View and edit cluster attributes.",
"attributes_of_cluster": "Attributes of the selected cluster",
"get_zigbee_attribute": "Get Zigbee Attribute",
"set_zigbee_attribute": "Set Zigbee Attribute",
"help_attribute_dropdown": "Select an attribute to view or set its value.",
"help_get_zigbee_attribute": "Get the value for the selected attribute.",
"help_set_zigbee_attribute": "Set attribute value for the specified cluster on the specified entity."
},
"cluster_commands": {
"header": "Cluster Commands",
"introduction": "View and issue cluster commands.",
"commands_of_cluster": "Commands of the selected cluster",
"issue_zigbee_command": "Issue Zigbee Command",
"help_command_dropdown": "Select a command to interact with."
} }
}, },
"zwave": { "zwave": {
"caption": "Z-Wave", "caption": "Z-Wave",
"description": "Manage your Z-Wave network", "description": "Manage your Z-Wave network",
"learn_more": "Learn more about Z-Wave",
"common": { "common": {
"value": "Value", "value": "Value",
"instance": "Instance", "instance": "Instance",
@ -1023,6 +1285,10 @@
"header": "Z-Wave Network Management", "header": "Z-Wave Network Management",
"introduction": "Run commands that affect the Z-Wave network. You won't get feedback on whether most commands succeeded, but you can check the OZW Log to try to find out." "introduction": "Run commands that affect the Z-Wave network. You won't get feedback on whether most commands succeeded, but you can check the OZW Log to try to find out."
}, },
"ozw_log": {
"header": "OZW Log",
"introduction": "View the log. 0 is the minimum (loads entire log) and 1000 is the maximum. Load will show a static log and tail will auto update with the last specified number of lines of the log."
},
"network_status": { "network_status": {
"network_stopped": "Z-Wave Network Stopped", "network_stopped": "Z-Wave Network Stopped",
"network_starting": "Starting Z-Wave Network...", "network_starting": "Starting Z-Wave Network...",
@ -1119,6 +1385,8 @@
"header": "Card Configuration", "header": "Card Configuration",
"pick_card": "Pick the card you want to add.", "pick_card": "Pick the card you want to add.",
"toggle_editor": "Toggle Editor", "toggle_editor": "Toggle Editor",
"show_visual_editor": "Show Visual Editor",
"show_code_editor": "Show Code Editor",
"add": "Add Card", "add": "Add Card",
"edit": "Edit", "edit": "Edit",
"delete": "Delete", "delete": "Delete",
@ -1138,17 +1406,29 @@
"migrate": "Migrate config" "migrate": "Migrate config"
}, },
"card": { "card": {
"alarm_panel": { "alarm-panel": {
"name": "Alarm Panel",
"available_states": "Available States" "available_states": "Available States"
}, },
"conditional": {
"name": "Conditional"
},
"config": { "config": {
"required": "Required", "required": "Required",
"optional": "Optional" "optional": "Optional"
}, },
"entities": { "entities": {
"name": "Entities",
"show_header_toggle": "Show Header Toggle?" "show_header_toggle": "Show Header Toggle?"
}, },
"entity-button": {
"name": "Entity Button"
},
"entity-filter": {
"name": "Entity Filter"
},
"gauge": { "gauge": {
"name": "Gauge",
"severity": { "severity": {
"define": "Define Severity?", "define": "Define Severity?",
"green": "Green", "green": "Green",
@ -1157,8 +1437,21 @@
} }
}, },
"glance": { "glance": {
"name": "Glance",
"columns": "Columns" "columns": "Columns"
}, },
"history-graph": {
"name": "History Graph"
},
"horizontal-stack": {
"name": "Horizontal Stack"
},
"iframe": {
"name": "iFrame"
},
"light": {
"name": "Light"
},
"generic": { "generic": {
"aspect_ratio": "Aspect Ratio", "aspect_ratio": "Aspect Ratio",
"camera_image": "Camera Entity", "camera_image": "Camera Entity",
@ -1184,17 +1477,50 @@
"url": "Url" "url": "Url"
}, },
"map": { "map": {
"name": "Map",
"geo_location_sources": "Geolocation Sources", "geo_location_sources": "Geolocation Sources",
"dark_mode": "Dark Mode?", "dark_mode": "Dark Mode?",
"default_zoom": "Default Zoom", "default_zoom": "Default Zoom",
"source": "Source" "source": "Source"
}, },
"markdown": { "markdown": {
"name": "Markdown",
"content": "Content" "content": "Content"
}, },
"media-control": {
"name": "Media Control"
},
"picture": {
"name": "Picture"
},
"picture-elements": {
"name": "Picture Elements"
},
"picture-entity": {
"name": "Picture Entity"
},
"picture-glance": {
"name": "Picture Glance"
},
"plant-status": {
"name": "Plant Status"
},
"sensor": { "sensor": {
"name": "Sensor",
"graph_detail": "Graph Detail", "graph_detail": "Graph Detail",
"graph_type": "Graph Type" "graph_type": "Graph Type"
},
"shopping-list": {
"name": "Shopping List"
},
"thermostat": {
"name": "Thermostat"
},
"vertical-stack": {
"name": "Vertical Stack"
},
"weather-forecast": {
"name": "Weather Forecast"
} }
} }
}, },
@ -1266,6 +1592,10 @@
"close": "Close", "close": "Close",
"submit": "Submit" "submit": "Submit"
}, },
"advanced_mode": {
"title": "Advanced Mode",
"description": "Home Assistant hides advanced features and options by default. You can make these features accessible by checking this toggle. This is a user-specific setting and does not impact other users using Home Assistant."
},
"refresh_tokens": { "refresh_tokens": {
"header": "Refresh Tokens", "header": "Refresh Tokens",
"description": "Each refresh token represents a login session. Refresh tokens will be automatically removed when you click log out. The following refresh tokens are currently active for your account.", "description": "Each refresh token represents a login session. Refresh tokens will be automatically removed when you click log out. The following refresh tokens are currently active for your account.",

View File

@ -46,7 +46,8 @@
"nativeName": "Euskara" "nativeName": "Euskara"
}, },
"fa": { "fa": {
"nativeName": "فارسی" "nativeName": "فارسی",
"isRTL": true
}, },
"fi": { "fi": {
"nativeName": "Suomi" "nativeName": "Suomi"

View File

@ -504,7 +504,12 @@
"leave": "Sortir" "leave": "Sortir"
}, },
"device": { "device": {
"label": "Dispositiu" "label": "Dispositiu",
"extra_fields": {
"above": "A sobre",
"below": "A sota",
"for": "Durada"
}
} }
}, },
"learn_more": "Més informació sobre els activadors" "learn_more": "Més informació sobre els activadors"
@ -1065,10 +1070,10 @@
}, },
"editor": { "editor": {
"edit_card": { "edit_card": {
"header": "Targeta de Configuració", "header": "Configuració de la targeta",
"save": "Desa", "save": "Desa",
"toggle_editor": "Commutar l'editor", "toggle_editor": "Commutar l'editor",
"pick_card": "Tria la targeta que vols afegir.", "pick_card": "Tria una targeta que vulguis afegir.",
"add": "Afegir targeta", "add": "Afegir targeta",
"edit": "Editar", "edit": "Editar",
"delete": "Elimina", "delete": "Elimina",

View File

@ -160,7 +160,7 @@
"not_home": "Ude" "not_home": "Ude"
}, },
"fan": { "fan": {
"off": "Slukket", "off": "Fra",
"on": "Tændt" "on": "Tændt"
}, },
"group": { "group": {
@ -504,7 +504,12 @@
"leave": "Forlad" "leave": "Forlad"
}, },
"device": { "device": {
"label": "Enhed" "label": "Enhed",
"extra_fields": {
"above": "Over",
"below": "Under",
"for": "Varighed"
}
} }
}, },
"learn_more": "Lær om udløsere" "learn_more": "Lær om udløsere"
@ -775,7 +780,8 @@
"enabled_label": "Aktivér enhed", "enabled_label": "Aktivér enhed",
"enabled_cause": "Deaktiveret af {cause}.", "enabled_cause": "Deaktiveret af {cause}.",
"enabled_description": "Deaktiverede enheder tilføjes ikke til Home Assistant.", "enabled_description": "Deaktiverede enheder tilføjes ikke til Home Assistant.",
"confirm_delete": "Er du sikker på, at du vil slette denne enhed?" "confirm_delete": "Er du sikker på, at du vil slette denne indtastning?",
"confirm_delete2": "Hvis du sletter en post, fjernes enheden ikke fra hjemme assistenten. Hvis du vil gøre dette, skal du fjerne integrationen ' {platform} ' fra Home Assistant."
} }
}, },
"person": { "person": {
@ -829,7 +835,7 @@
"caption": "Gør kun noget, hvis ..." "caption": "Gør kun noget, hvis ..."
}, },
"actions": { "actions": {
"caption": "Når noget udløses ..." "caption": "Når noget påvirkes ..."
} }
} }
} }
@ -909,7 +915,7 @@
"description": "Dette vil skjule sidepanelet som standard, svarende til den mobile oplevelse." "description": "Dette vil skjule sidepanelet som standard, svarende til den mobile oplevelse."
}, },
"vibrate": { "vibrate": {
"header": "Vibrere", "header": "Vibrer",
"description": "Aktivér eller deaktiver vibrationer på denne enhed, når du styrer enheder." "description": "Aktivér eller deaktiver vibrationer på denne enhed, når du styrer enheder."
} }
}, },
@ -1058,7 +1064,8 @@
"navigate_to": "Naviger til {location}", "navigate_to": "Naviger til {location}",
"toggle": "Skift {name}", "toggle": "Skift {name}",
"call_service": "Kald service {name}", "call_service": "Kald service {name}",
"more_info": "Vis mere-info: {name}" "more_info": "Vis mere-info: {name}",
"url": "Åbn vindue til {url_path}"
} }
}, },
"editor": { "editor": {
@ -1113,6 +1120,9 @@
"required": "Påkrævet", "required": "Påkrævet",
"optional": "Valgfri" "optional": "Valgfri"
}, },
"entities": {
"show_header_toggle": "Vis omskifter ved overskrift?"
},
"gauge": { "gauge": {
"severity": { "severity": {
"define": "Definer alvorlighed?", "define": "Definer alvorlighed?",
@ -1131,6 +1141,7 @@
"entities": "Enheder", "entities": "Enheder",
"entity": "Enhed", "entity": "Enhed",
"hold_action": "Hold handling", "hold_action": "Hold handling",
"hours_to_show": "Vis i antal timer",
"icon": "Ikon", "icon": "Ikon",
"icon_height": "Ikonhøjde", "icon_height": "Ikonhøjde",
"image": "Sti til billede", "image": "Sti til billede",
@ -1432,12 +1443,12 @@
"manuf": "af {manufacturer}", "manuf": "af {manufacturer}",
"no_area": "Intet område", "no_area": "Intet område",
"services": { "services": {
"reconfigure": "Genkonfigurer ZHA-enhed (helbred enhed). Brug dette hvis du har problemer med enheden. Hvis den pågældende enhed er en batteridrevet enhed skal du sørge for at den er vågen og accepterer kommandoer når du bruger denne service.", "reconfigure": "Rekonfigurer ZHA-enhed (helbred enhed). Brug dette hvis du har problemer med enheden. Hvis den pågældende enhed er en batteridrevet enhed skal du sørge for at den er vågen og accepterer kommandoer når du bruger denne service.",
"updateDeviceName": "Angiv et brugerdefineret navn til denne enhed i enhedsopsætningen", "updateDeviceName": "Angiv et brugerdefineret navn til denne enhed i enhedsopsætningen",
"remove": "Fjern en enhed fra Zigbee-netværket." "remove": "Fjern en enhed fra Zigbee-netværket."
}, },
"zha_device_card": { "zha_device_card": {
"device_name_placeholder": "Navn", "device_name_placeholder": "Bruger tildelt navn",
"area_picker_label": "Område", "area_picker_label": "Område",
"update_name_button": "Opdater navn" "update_name_button": "Opdater navn"
} }

View File

@ -504,7 +504,12 @@
"leave": "Verlassen" "leave": "Verlassen"
}, },
"device": { "device": {
"label": "Gerät" "label": "Gerät",
"extra_fields": {
"above": "Über",
"below": "Unter",
"for": "Dauer"
}
} }
}, },
"learn_more": "Erfahre mehr über Auslöser" "learn_more": "Erfahre mehr über Auslöser"
@ -554,6 +559,12 @@
}, },
"device": { "device": {
"label": "Gerät" "label": "Gerät"
},
"and": {
"label": "Und"
},
"or": {
"label": "Oder"
} }
}, },
"learn_more": "Erfahre mehr über Bedingungen" "learn_more": "Erfahre mehr über Bedingungen"
@ -596,7 +607,11 @@
"learn_more": "Erfahre mehr über Aktionen" "learn_more": "Erfahre mehr über Aktionen"
}, },
"load_error_not_editable": "Nur Automatisierungen in automations.yaml sind editierbar.", "load_error_not_editable": "Nur Automatisierungen in automations.yaml sind editierbar.",
"load_error_unknown": "Fehler beim Laden der Automatisierung ({err_no})." "load_error_unknown": "Fehler beim Laden der Automatisierung ({err_no}).",
"description": {
"label": "Beschreibung",
"placeholder": "Optionale Beschreibung"
}
} }
}, },
"script": { "script": {
@ -754,7 +769,8 @@
"unavailable": "(nicht verfügbar)", "unavailable": "(nicht verfügbar)",
"introduction": "Der Home Assistant führt eine Registrierung aller Entitäten, die er je gesehen hat und die eindeutig identifiziert werden können. Jeder dieser Entitäten wird eine Entitäts-ID zugewiesen, die nur für diese Entität reserviert ist.", "introduction": "Der Home Assistant führt eine Registrierung aller Entitäten, die er je gesehen hat und die eindeutig identifiziert werden können. Jeder dieser Entitäten wird eine Entitäts-ID zugewiesen, die nur für diese Entität reserviert ist.",
"introduction2": "Verwende die Entitätsregistrierung, um den Namen zu überschreiben, die Entität-ID zu ändern oder den Eintrag aus dem Home Assistant zu entfernen. Beachte, dass das Entfernen des Entitätsregistrierungseintrags die Entität nicht löscht. Folge dazu dem Link unten und entferne ihn in der Integrationsseite.", "introduction2": "Verwende die Entitätsregistrierung, um den Namen zu überschreiben, die Entität-ID zu ändern oder den Eintrag aus dem Home Assistant zu entfernen. Beachte, dass das Entfernen des Entitätsregistrierungseintrags die Entität nicht löscht. Folge dazu dem Link unten und entferne ihn in der Integrationsseite.",
"integrations_page": "Integrationsseite" "integrations_page": "Integrationsseite",
"show_disabled": "Anzeigen deaktivierter Entitäten"
}, },
"editor": { "editor": {
"unavailable": "Diese Entität ist derzeit nicht verfügbar.", "unavailable": "Diese Entität ist derzeit nicht verfügbar.",
@ -763,7 +779,9 @@
"update": "UPDATE", "update": "UPDATE",
"enabled_label": "Entität aktivieren", "enabled_label": "Entität aktivieren",
"enabled_cause": "Deaktiviert durch {cause}.", "enabled_cause": "Deaktiviert durch {cause}.",
"enabled_description": "Deaktivierte Entitäten werden in Home Assistant nicht hinzugefügt." "enabled_description": "Deaktivierte Entitäten werden in Home Assistant nicht hinzugefügt.",
"confirm_delete": "Möchten Sie diesen Eintrag wirklich löschen?",
"confirm_delete2": "Durch das Löschen eines Eintrags wird die Entität nicht aus Home Assistant entfernt. Dazu müssen Sie die Integration '{platform}' von Home Assistant entfernen."
} }
}, },
"person": { "person": {
@ -808,7 +826,18 @@
}, },
"devices": { "devices": {
"caption": "Geräte", "caption": "Geräte",
"description": "Verwalte verbundene Geräte" "description": "Verwalte verbundene Geräte",
"automation": {
"triggers": {
"caption": "Tu nur etwas, wenn..."
},
"conditions": {
"caption": "Tu nur etwas, wenn..."
},
"actions": {
"caption": "Wenn etwas ausgelöst wird ..."
}
}
} }
}, },
"profile": { "profile": {
@ -886,7 +915,8 @@
"description": "Dies blendet die Seitenleiste standardmäßig aus, ähnlich der Nutzung auf Mobilgeräten." "description": "Dies blendet die Seitenleiste standardmäßig aus, ähnlich der Nutzung auf Mobilgeräten."
}, },
"vibrate": { "vibrate": {
"header": "Vibrieren" "header": "Vibrieren",
"description": "Aktivieren oder deaktivieren Sie die Vibration an diesem Gerät, wenn Sie Geräte steuern."
} }
}, },
"page-authorize": { "page-authorize": {
@ -1034,7 +1064,8 @@
"navigate_to": "Navigiere zu {location}", "navigate_to": "Navigiere zu {location}",
"toggle": "{name} umschalten", "toggle": "{name} umschalten",
"call_service": "Dienst {name} ausführen", "call_service": "Dienst {name} ausführen",
"more_info": "Zeige weitere Informationen: {name}" "more_info": "Zeige weitere Informationen: {name}",
"url": "Fenster zu {url_path} öffnen"
} }
}, },
"editor": { "editor": {
@ -1080,6 +1111,66 @@
"edit_lovelace": { "edit_lovelace": {
"header": "Titel deiner Lovelace UI", "header": "Titel deiner Lovelace UI",
"explanation": "Dieser Titel wird überhalb aller deiner Lovelace Ansichten gezeigt." "explanation": "Dieser Titel wird überhalb aller deiner Lovelace Ansichten gezeigt."
},
"card": {
"alarm_panel": {
"available_states": "Verfügbare Zustände"
},
"config": {
"required": "Benötigt",
"optional": "Optional"
},
"entities": {
"show_header_toggle": "Namen anzeigen?"
},
"gauge": {
"severity": {
"define": "Schweregrad definieren?",
"green": "Grün",
"red": "Rot",
"yellow": "Gelb"
}
},
"glance": {
"columns": "Spalten"
},
"generic": {
"aspect_ratio": "Seitenverhältnis",
"camera_image": "Kamera-Entität",
"camera_view": "Kameraansicht",
"entities": "Ungenutzte Elemente",
"entity": "Entität",
"hold_action": "Halte-Aktion",
"hours_to_show": "Stunden",
"icon": "Symbol",
"icon_height": "Symbol Höhe",
"image": "Bildpfad",
"maximum": "Maximum",
"minimum": "Minimum",
"name": "Name",
"refresh_interval": "Aktualisierungsintervall",
"show_icon": "Symbol anzeigen?",
"show_name": "Namen anzeigen?",
"show_state": "Status anzeigen?",
"tap_action": "Tipp-Aktion",
"title": "Titel",
"theme": "Aussehen",
"unit": "Einheit",
"url": "Url"
},
"map": {
"geo_location_sources": "Geolocation-Quellen",
"dark_mode": "Dunkler Modus?",
"default_zoom": "Standard-Zoom",
"source": "Quelle"
},
"markdown": {
"content": "Inhalt"
},
"sensor": {
"graph_detail": "Diagrammdetail",
"graph_type": "Typ"
}
} }
}, },
"menu": { "menu": {
@ -1352,6 +1443,7 @@
"manuf": "von {manufacturer}", "manuf": "von {manufacturer}",
"no_area": "Kein Bereich", "no_area": "Kein Bereich",
"services": { "services": {
"reconfigure": "Konfigurieren Sie das ZHA-Gerät neu (Gerät heilen). Verwenden Sie diese Option, wenn Sie Probleme mit dem Gerät haben. Wenn es sich bei dem fraglichen Gerät um ein batteriebetriebenes Gerät handelt, vergewissern Sie sich, dass es wach ist und Befehle akzeptiert, wenn Sie diesen Dienst nutzen.",
"updateDeviceName": "Lege einen benutzerdefinierten Namen für dieses Gerät in der Geräteregistrierung fest.", "updateDeviceName": "Lege einen benutzerdefinierten Namen für dieses Gerät in der Geräteregistrierung fest.",
"remove": "Entfernen Sie ein Gerät aus dem ZigBee-Netzwerk." "remove": "Entfernen Sie ein Gerät aus dem ZigBee-Netzwerk."
}, },
@ -1437,7 +1529,7 @@
"away": "Abwesend", "away": "Abwesend",
"boost": "Maximal", "boost": "Maximal",
"comfort": "Komfort", "comfort": "Komfort",
"home": "Zu Hause", "home": "Zuhause",
"sleep": "Schlafen", "sleep": "Schlafen",
"activity": "Aktivität" "activity": "Aktivität"
}, },

View File

@ -763,7 +763,8 @@
"update": "ΕΝΗΜΕΡΩΣΗ", "update": "ΕΝΗΜΕΡΩΣΗ",
"enabled_label": "Ενεργοποίηση οντότητας", "enabled_label": "Ενεργοποίηση οντότητας",
"enabled_cause": "Απενεργοποιήθηκε από τo {cause}.", "enabled_cause": "Απενεργοποιήθηκε από τo {cause}.",
"enabled_description": "Απενεργοποιημένες οντότητες δεν θα προστεθούν στον Home Assistant" "enabled_description": "Απενεργοποιημένες οντότητες δεν θα προστεθούν στον Home Assistant",
"confirm_delete2": "Η διαγραφή μιας καταχώρησης δεν θα καταργήσει την οντότητα από το Home Assistant. Για να γίνει αυτό, θα πρέπει να καταργήσετε την ενσωμάτωση '{platform}' από το Home Assistant."
} }
}, },
"person": { "person": {

View File

@ -504,7 +504,12 @@
"leave": "Leave" "leave": "Leave"
}, },
"device": { "device": {
"label": "Device" "label": "Device",
"extra_fields": {
"above": "Above",
"below": "Below",
"for": "Duration"
}
} }
}, },
"learn_more": "Learn more about triggers" "learn_more": "Learn more about triggers"

View File

@ -160,7 +160,7 @@
"not_home": "Fuera de Casa" "not_home": "Fuera de Casa"
}, },
"fan": { "fan": {
"off": "Apagado", "off": "Desactivado",
"on": "Encendido" "on": "Encendido"
}, },
"group": { "group": {

View File

@ -504,7 +504,12 @@
"leave": "Salir" "leave": "Salir"
}, },
"device": { "device": {
"label": "Dispositivo" "label": "Dispositivo",
"extra_fields": {
"above": "Por encima de",
"below": "Por debajo de",
"for": "Duración"
}
} }
}, },
"learn_more": "Aprende más sobre los desencadenantes" "learn_more": "Aprende más sobre los desencadenantes"

View File

@ -691,7 +691,10 @@
}, },
"profile": { "profile": {
"push_notifications": { "push_notifications": {
"description": "ارسال اعلان ها به این دستگاه" "header": "ارسال اعلانها",
"description": "ارسال اعلانها به این دستگاه",
"push_notifications": "ارسال اعلانها",
"link_promo": "بیشتر بدانید"
}, },
"refresh_tokens": { "refresh_tokens": {
"header": "تازه کردن نشانه", "header": "تازه کردن نشانه",
@ -713,7 +716,10 @@
}, },
"current_user": "شما در حال حاضر به عنوان {fullName} وارد شده اید.", "current_user": "شما در حال حاضر به عنوان {fullName} وارد شده اید.",
"change_password": { "change_password": {
"header": "تغییر رمز عبور" "header": "تغییر رمز عبور",
"current_password": "رمز فعلی",
"new_password": "رمز جدید",
"confirm_new_password": "تائید رمز جدید"
}, },
"mfa": { "mfa": {
"confirm_disable": "آیا مطمئن هستید که میخواهید {name} غیرفعال کنید؟" "confirm_disable": "آیا مطمئن هستید که میخواهید {name} غیرفعال کنید؟"
@ -813,7 +819,7 @@
}, },
"core-config": { "core-config": {
"intro": "سلام {name} ، به دستیار خانگی خوش آمدید چگونه می خواهید خانه خود را نام ببرید؟", "intro": "سلام {name} ، به دستیار خانگی خوش آمدید چگونه می خواهید خانه خود را نام ببرید؟",
"intro_location": "ما می خواهیم بدانیم کجا زندگی می کنیم این اطلاعات برای نمایش اطلاعات و تنظیم خودکار اتوماسیون مبتنی بر آفتاب کمک خواهد کرد. این اطلاعات در خارج از شبکه شما به اشتراک گذاشته نمیشود .", "intro_location": "ما می خواهیم بدانیم کجا زندگی می کنید این اطلاعات برای نمایش اطلاعات و تنظیم خودکار اتوماسیون مبتنی بر آخورشید کمک خواهد کرد. این اطلاعات در خارج از شبکه شما به اشتراک گذاشته نمیشود .",
"intro_location_detect": "ما می تواند کمک به شما در پر کردن این اطلاعات با ساخت یک درخواست به یک سرویس خارجی.", "intro_location_detect": "ما می تواند کمک به شما در پر کردن این اطلاعات با ساخت یک درخواست به یک سرویس خارجی.",
"location_name_default": "خانه", "location_name_default": "خانه",
"button_detect": "تشخیص", "button_detect": "تشخیص",
@ -1078,7 +1084,9 @@
"confirm": "ذخیره ورود به سیستم" "confirm": "ذخیره ورود به سیستم"
}, },
"notification_drawer": { "notification_drawer": {
"click_to_configure": "برای پیکربندی {entity} روی دکمه کلیک کنید" "click_to_configure": "برای پیکربندی {entity} روی دکمه کلیک کنید",
"empty": "بدون اعلان",
"title": "اعلانها"
}, },
"common": { "common": {
"save": "ذخیره" "save": "ذخیره"

View File

@ -1001,7 +1001,7 @@
}, },
"core-config": { "core-config": {
"intro": "Hei {name}, tervetuloa Home Assistant -käyttäjäksi. Kuinka haluaisit nimetä uuden kotisi?", "intro": "Hei {name}, tervetuloa Home Assistant -käyttäjäksi. Kuinka haluaisit nimetä uuden kotisi?",
"intro_location": "Haluaisimme tietää, missä asut. Nämä tiedot auttavat näyttämään tietoja ja perustamaan aurinkopohjaisia automaatioita. Nämä tiedot eivät koskaan jaeta oman verkkosi ulkopuolella.", "intro_location": "Haluaisimme tietää, missä asut. Nämä tiedot auttavat näyttämään tietoja ja perustamaan aurinkoon perustuvia automaatioita. Näitä tietoja ei koskaan jaeta oman verkkosi ulkopuolelle.",
"intro_location_detect": "Voimme auttaa sinua täyttämään nämä tiedot tekemällä kertaluonteisen pyynnön ulkoiselle palvelulle.", "intro_location_detect": "Voimme auttaa sinua täyttämään nämä tiedot tekemällä kertaluonteisen pyynnön ulkoiselle palvelulle.",
"location_name_default": "Koti", "location_name_default": "Koti",
"button_detect": "Havaitse", "button_detect": "Havaitse",

View File

@ -33,12 +33,12 @@
}, },
"automation": { "automation": {
"off": "Off", "off": "Off",
"on": "On" "on": "[%key_id:state::default::on%]"
}, },
"binary_sensor": { "binary_sensor": {
"default": { "default": {
"off": "Off", "off": "Off",
"on": "On" "on": "[%key_id:state::default::on%]"
}, },
"moisture": { "moisture": {
"off": "Sec", "off": "Sec",
@ -118,8 +118,8 @@
} }
}, },
"calendar": { "calendar": {
"off": "Off", "off": "[%key_id:state::default::off%]",
"on": "On" "on": "[%key_id:state::default::on%]"
}, },
"camera": { "camera": {
"recording": "Enregistrement", "recording": "Enregistrement",
@ -165,7 +165,7 @@
}, },
"group": { "group": {
"off": "Off", "off": "Off",
"on": "On", "on": "[%key_id:state::default::on%]",
"home": "Présent", "home": "Présent",
"not_home": "Absent", "not_home": "Absent",
"open": "Ouvert", "open": "Ouvert",
@ -210,19 +210,19 @@
"scening": "Scénario" "scening": "Scénario"
}, },
"script": { "script": {
"off": "Off", "off": "[%key_id:state::default::off%]",
"on": "On" "on": "[%key_id:state::default::on%]"
}, },
"sensor": { "sensor": {
"off": "Off", "off": "[%key_id:state::default::off%]",
"on": "On" "on": "[%key_id:state::default::on%]"
}, },
"sun": { "sun": {
"above_horizon": "Au-dessus de l'horizon", "above_horizon": "Au-dessus de l'horizon",
"below_horizon": "Sous lhorizon" "below_horizon": "Sous lhorizon"
}, },
"switch": { "switch": {
"off": "Off", "off": "[%key_id:state::default::off%]",
"on": "On" "on": "On"
}, },
"zwave": { "zwave": {
@ -284,7 +284,7 @@
"alarm_control_panel": { "alarm_control_panel": {
"armed": "Activé", "armed": "Activé",
"disarmed": "Désactivé", "disarmed": "Désactivé",
"armed_home": "Armé", "armed_home": "Activé",
"armed_away": "Activé", "armed_away": "Activé",
"armed_night": "Activé", "armed_night": "Activé",
"pending": "En cours", "pending": "En cours",
@ -294,7 +294,7 @@
"armed_custom_bypass": "Activé" "armed_custom_bypass": "Activé"
}, },
"device_tracker": { "device_tracker": {
"home": "Maison", "home": "Présent",
"not_home": "Absent" "not_home": "Absent"
}, },
"person": { "person": {
@ -504,7 +504,12 @@
"leave": "Quitte" "leave": "Quitte"
}, },
"device": { "device": {
"label": "Équipements" "label": "Équipements",
"extra_fields": {
"above": "Au-dessus de",
"below": "Au-dessous de",
"for": "Durée"
}
} }
}, },
"learn_more": "En savoir plus sur les déclencheurs" "learn_more": "En savoir plus sur les déclencheurs"
@ -1524,7 +1529,7 @@
"away": "Absent", "away": "Absent",
"boost": "Renforcer", "boost": "Renforcer",
"comfort": "Confort", "comfort": "Confort",
"home": "Accueil", "home": "Présent",
"sleep": "Veille", "sleep": "Veille",
"activity": "Activité" "activity": "Activité"
}, },

View File

@ -78,6 +78,7 @@
"home": "घर" "home": "घर"
}, },
"fan": { "fan": {
"off": "बंद",
"on": "चालू" "on": "चालू"
}, },
"group": { "group": {

View File

@ -504,7 +504,12 @@
"leave": "Távozás" "leave": "Távozás"
}, },
"device": { "device": {
"label": "Eszköz" "label": "Eszköz",
"extra_fields": {
"above": "Felett",
"below": "Alatt",
"for": "Időtartam"
}
} }
}, },
"learn_more": "Tudj meg többet a triggerekről" "learn_more": "Tudj meg többet a triggerekről"

View File

@ -454,7 +454,11 @@
"leave": "Brottför" "leave": "Brottför"
}, },
"device": { "device": {
"label": "Tæki" "label": "Tæki",
"extra_fields": {
"above": "Yfir",
"below": "Undir"
}
} }
}, },
"learn_more": "Læra meira um kveikjur" "learn_more": "Læra meira um kveikjur"

View File

@ -504,7 +504,12 @@
"leave": "Uscita" "leave": "Uscita"
}, },
"device": { "device": {
"label": "Dispositivo" "label": "Dispositivo",
"extra_fields": {
"above": "Sopra",
"below": "Sotto",
"for": "Durata"
}
} }
}, },
"learn_more": "Per saperne di più sui trigger" "learn_more": "Per saperne di più sui trigger"

View File

@ -118,6 +118,7 @@
"off": "オフ", "off": "オフ",
"on": "オン", "on": "オン",
"home": "在宅", "home": "在宅",
"not_home": "外出",
"ok": "OK" "ok": "OK"
}, },
"input_boolean": { "input_boolean": {

View File

@ -504,7 +504,12 @@
"leave": "퇴장" "leave": "퇴장"
}, },
"device": { "device": {
"label": "기기" "label": "기기",
"extra_fields": {
"above": "이상",
"below": "이하",
"for": "동안"
}
} }
}, },
"learn_more": "트리거에 대해 더 알아보기" "learn_more": "트리거에 대해 더 알아보기"
@ -554,6 +559,12 @@
}, },
"device": { "device": {
"label": "기기" "label": "기기"
},
"and": {
"label": "다중조건 (그리고)"
},
"or": {
"label": "다중조건 (또는)"
} }
}, },
"learn_more": "조건에 대해 더 알아보기" "learn_more": "조건에 대해 더 알아보기"
@ -596,7 +607,11 @@
"learn_more": "동작에 대해 더 알아보기" "learn_more": "동작에 대해 더 알아보기"
}, },
"load_error_not_editable": "automations.yaml 의 자동화만 편집할 수 있습니다.", "load_error_not_editable": "automations.yaml 의 자동화만 편집할 수 있습니다.",
"load_error_unknown": "자동화를 읽어오는 도중 오류가 발생했습니다 ({err_no})." "load_error_unknown": "자동화를 읽어오는 도중 오류가 발생했습니다 ({err_no}).",
"description": {
"label": "설명",
"placeholder": "부가설명"
}
} }
}, },
"script": { "script": {
@ -711,7 +726,7 @@
"caption": "ZHA", "caption": "ZHA",
"description": "Zigbee 홈 자동화 네트워크 관리", "description": "Zigbee 홈 자동화 네트워크 관리",
"services": { "services": {
"reconfigure": "ZHA 기기를 다시 구성 합니다. (장치 복구). 기기에 문제가 있는 경우 사용해주세요. 기기가 배터리로 작동하는 경우, 이 서비스를 사용할 때 기기가 켜져있고 통신이 가능한 상태인지 확인해주세요.", "reconfigure": "ZHA 기기를 다시 구성 합니다. (기기 복구). 기기에 문제가 있는 경우 사용해주세요. 기기가 배터리로 작동하는 경우, 이 서비스를 사용할 때 기기가 켜져있고 통신이 가능한 상태인지 확인해주세요.",
"updateDeviceName": "이 기기의 사용자 정의 이름을 기기 레지스트리에 설정합니다.", "updateDeviceName": "이 기기의 사용자 정의 이름을 기기 레지스트리에 설정합니다.",
"remove": "Zigbee 네트워크에서 기기 제거" "remove": "Zigbee 네트워크에서 기기 제거"
}, },
@ -754,7 +769,8 @@
"unavailable": "(사용불가)", "unavailable": "(사용불가)",
"introduction": "Home Assistant 는 구성요소의 식별을 위해 모든 구성요소에 고유한 레지스트리를 부여합니다. 각각의 구성요소들은 자신만의 고유한 구성요소 ID 를 가집니다.", "introduction": "Home Assistant 는 구성요소의 식별을 위해 모든 구성요소에 고유한 레지스트리를 부여합니다. 각각의 구성요소들은 자신만의 고유한 구성요소 ID 를 가집니다.",
"introduction2": "구성요소를 편집하여 이름을 대체하거나 구성요소 ID를 변경하고 Home Assistant 에서 항목을 제거할 수 있습니다. 단, 구성요소 편집창에서 구성요소를 삭제해도 구성요소가 완전히 제거되는 것은 아닙니다. 완전히 제거하려면, 아래 링크를 따라 통합 구성요소 페이지에서 제거해주세요.", "introduction2": "구성요소를 편집하여 이름을 대체하거나 구성요소 ID를 변경하고 Home Assistant 에서 항목을 제거할 수 있습니다. 단, 구성요소 편집창에서 구성요소를 삭제해도 구성요소가 완전히 제거되는 것은 아닙니다. 완전히 제거하려면, 아래 링크를 따라 통합 구성요소 페이지에서 제거해주세요.",
"integrations_page": "통합 구성요소 페이지" "integrations_page": "통합 구성요소 페이지",
"show_disabled": "비활성화 된 구성요소 표시"
}, },
"editor": { "editor": {
"unavailable": "이 구성요소는 현재 사용할 수 없습니다.", "unavailable": "이 구성요소는 현재 사용할 수 없습니다.",
@ -763,7 +779,9 @@
"update": "업데이트", "update": "업데이트",
"enabled_label": "구성요소 활성화", "enabled_label": "구성요소 활성화",
"enabled_cause": "{cause} 에 의해 비활성화 되었습니다.", "enabled_cause": "{cause} 에 의해 비활성화 되었습니다.",
"enabled_description": "비활성화 된 구성요소는 Home Assistant 에 추가되지 않습니다." "enabled_description": "비활성화 된 구성요소는 Home Assistant 에 추가되지 않습니다.",
"confirm_delete": "이 구성요소를 제거 하시겠습니까?",
"confirm_delete2": "구성요소 항목을 제거해도 Home Assistant 에서 실제로 구성요소가 제거되는것은 아닙니다. 완전히 제거하려면, Home Assistant 에서 '{platform}' 통합 구성요소를 제거해주세요."
} }
}, },
"person": { "person": {
@ -771,9 +789,9 @@
"description": "Home Assistant 가 추적하는 구성원을 관리합니다", "description": "Home Assistant 가 추적하는 구성원을 관리합니다",
"detail": { "detail": {
"name": "이름", "name": "이름",
"device_tracker_intro": "이 구성원에게 속한 장치를 선택해주세요.", "device_tracker_intro": "이 구성원에게 속한 기기를 선택해주세요.",
"device_tracker_picked": "장치 추적 대상", "device_tracker_picked": "추적 대상 기기",
"device_tracker_pick": "추적 할 장치 선택" "device_tracker_pick": "추적 할 기기 선택"
} }
}, },
"server_control": { "server_control": {
@ -808,7 +826,18 @@
}, },
"devices": { "devices": {
"caption": "기기", "caption": "기기",
"description": "연결된 기기 관리" "description": "연결된 기기 관리",
"automation": {
"triggers": {
"caption": "...일 때 뭔가를 실행"
},
"conditions": {
"caption": "...인 경우 뭔가를 실행"
},
"actions": {
"caption": "뭔가 트리거 되었을 때...."
}
}
} }
}, },
"profile": { "profile": {
@ -884,6 +913,10 @@
"force_narrow": { "force_narrow": {
"header": "항상 사이드바 숨기기", "header": "항상 사이드바 숨기기",
"description": "모바일 환경과 마찬가지로 기본적으로 사이드 바가 숨겨집니다." "description": "모바일 환경과 마찬가지로 기본적으로 사이드 바가 숨겨집니다."
},
"vibrate": {
"header": "진동효과",
"description": "기기를 제어 할 때 이 기기에서 진동을 활성화 또는 비활성화합니다."
} }
}, },
"page-authorize": { "page-authorize": {
@ -1031,7 +1064,8 @@
"navigate_to": "{location} 로(으로) 이동", "navigate_to": "{location} 로(으로) 이동",
"toggle": "{name} 토글", "toggle": "{name} 토글",
"call_service": "{name} 서비스 호출", "call_service": "{name} 서비스 호출",
"more_info": "추가 정보 표시: {name}" "more_info": "추가 정보 표시: {name}",
"url": "{url_path} 창 열기"
} }
}, },
"editor": { "editor": {
@ -1077,6 +1111,66 @@
"edit_lovelace": { "edit_lovelace": {
"header": "Lovelace UI 의 제목", "header": "Lovelace UI 의 제목",
"explanation": "이 제목은 Lovelace 의 모든 화면의 상단에 표시됩니다." "explanation": "이 제목은 Lovelace 의 모든 화면의 상단에 표시됩니다."
},
"card": {
"alarm_panel": {
"available_states": "사용 가능한 상태요소"
},
"config": {
"required": "필수 요소",
"optional": "선택사항"
},
"entities": {
"show_header_toggle": "헤더 토글 표시"
},
"gauge": {
"severity": {
"define": "게이지 구간 정의하기",
"green": "초록",
"red": "빨강",
"yellow": "노랑"
}
},
"glance": {
"columns": "열"
},
"generic": {
"aspect_ratio": "종횡비",
"camera_image": "카메라 구성요소",
"camera_view": "카메라 뷰",
"entities": "구성요소",
"entity": "구성요소",
"hold_action": "길게 누르기 동작",
"hours_to_show": "표시 시간",
"icon": "아이콘",
"icon_height": "아이콘 높이",
"image": "이미지 경로",
"maximum": "최대",
"minimum": "최소",
"name": "이름",
"refresh_interval": "새로 고침 간격",
"show_icon": "아이콘 표시",
"show_name": "이름 표시",
"show_state": "상태 표시",
"tap_action": "탭 동작",
"title": "제목",
"theme": "테마",
"unit": "단위",
"url": "Url"
},
"map": {
"geo_location_sources": "위치정보 소스",
"dark_mode": "어둡게 표시",
"default_zoom": "기본 확대",
"source": "소스"
},
"markdown": {
"content": "내용"
},
"sensor": {
"graph_detail": "그래프 세부묘사 정도",
"graph_type": "그래프 유형"
}
} }
}, },
"menu": { "menu": {
@ -1349,7 +1443,7 @@
"manuf": "{manufacturer} 제조", "manuf": "{manufacturer} 제조",
"no_area": "영역 없음", "no_area": "영역 없음",
"services": { "services": {
"reconfigure": "ZHA 장치를 다시 구성 합니다. (장치 복구). 장치에 문제가 있는 경우 사용해주세요. 장치가 배터리로 작동하는 경우, 이 서비스를 사용할 때 장치가 켜져있고 통신이 가능한 상태여야 합니다.", "reconfigure": "ZHA 기기를 다시 구성 합니다. (기기 복구). 기기에 문제가 있는 경우 사용해주세요. 기기가 배터리로 작동하는 경우, 이 서비스를 사용할 때 기기가 켜져있고 통신이 가능한 상태인지 확인해주세요.",
"updateDeviceName": "이 기기의 사용자 정의 이름을 기기 레지스트리에 설정합니다.", "updateDeviceName": "이 기기의 사용자 정의 이름을 기기 레지스트리에 설정합니다.",
"remove": "Zigbee 네트워크에서 기기를 제거해주세요." "remove": "Zigbee 네트워크에서 기기를 제거해주세요."
}, },
@ -1367,7 +1461,7 @@
}, },
"notification_drawer": { "notification_drawer": {
"click_to_configure": "버튼을 클릭하여 {entity} 을(를) 구성", "click_to_configure": "버튼을 클릭하여 {entity} 을(를) 구성",
"empty": "알림 없음", "empty": "알림 내용이 없습니다.",
"title": "알림" "title": "알림"
} }
}, },
@ -1381,7 +1475,7 @@
"configurator": "구성", "configurator": "구성",
"conversation": "대화", "conversation": "대화",
"cover": "여닫이", "cover": "여닫이",
"device_tracker": "추적 장치", "device_tracker": "추적 기기",
"fan": "송풍기", "fan": "송풍기",
"history_graph": "기록그래프", "history_graph": "기록그래프",
"group": "그룹", "group": "그룹",

View File

@ -504,7 +504,12 @@
"leave": "Verloossen" "leave": "Verloossen"
}, },
"device": { "device": {
"label": "Apparat" "label": "Apparat",
"extra_fields": {
"above": "Iwwert",
"below": "Ënnert",
"for": "Dauer"
}
} }
}, },
"learn_more": "Méi iwwert Ausléiser liesen" "learn_more": "Méi iwwert Ausléiser liesen"

View File

@ -97,6 +97,10 @@
"on": "Įjungta", "on": "Įjungta",
"manual": "Rankinis" "manual": "Rankinis"
}, },
"fan": {
"off": "Išjungta",
"on": "Įjungta"
},
"group": { "group": {
"off": "Išjungta", "off": "Išjungta",
"on": "Įjungta", "on": "Įjungta",

View File

@ -753,7 +753,7 @@
"header": "Vienību Reģistrs", "header": "Vienību Reģistrs",
"unavailable": "(nav pieejams)", "unavailable": "(nav pieejams)",
"introduction": "Home Assistant uztur katras vienības reģistru, kuru var unikāli identificēt. Katrai no šīm vienībām tiks piešķirts vienības ID, kas tiks rezervēts tikai šai vienībai.", "introduction": "Home Assistant uztur katras vienības reģistru, kuru var unikāli identificēt. Katrai no šīm vienībām tiks piešķirts vienības ID, kas tiks rezervēts tikai šai vienībai.",
"introduction2": "Izmantojiet vienību reģistru, lai ignorētu vārdu, mainītu vienību ID vai noņemtu ierakstu no Home Assistant. Ņemiet vērā: noņemot vienības reģistra ierakstu, vienība netiks noņemta. Lai to izdarītu, sekojiet zemāk esošajai saitei un noņemiet to no integrācijas lapas.", "introduction2": "Izmantojiet vienību reģistru, lai piešķirtu vārdu, mainītu vienību ID vai noņemtu ierakstu no Home Assistant. Ņemiet vērā: noņemot vienības reģistra ierakstu, vienība netiks noņemta. Lai to izdarītu, sekojiet zemāk esošajai saitei un noņemiet to no integrācijas lapas.",
"integrations_page": "Integrāciju lapa" "integrations_page": "Integrāciju lapa"
}, },
"editor": { "editor": {
@ -1098,7 +1098,7 @@
"page-demo": { "page-demo": {
"cards": { "cards": {
"demo": { "demo": {
"demo_by": "{name} ", "demo_by": "{name}",
"next_demo": "Nākamā demonstrācija", "next_demo": "Nākamā demonstrācija",
"introduction": "Laipni lūgts mājās! Jūs esat sasniedzis Home Assistant demonstrāciju, kurā mēs parādām labākos lietotāja kopienas izveidotos lietotāja interfeisus.", "introduction": "Laipni lūgts mājās! Jūs esat sasniedzis Home Assistant demonstrāciju, kurā mēs parādām labākos lietotāja kopienas izveidotos lietotāja interfeisus.",
"learn_more": "Uzziniet vairāk par Home Assistant" "learn_more": "Uzziniet vairāk par Home Assistant"
@ -1316,7 +1316,7 @@
"dialogs": { "dialogs": {
"more_info_settings": { "more_info_settings": {
"save": "Saglabāt", "save": "Saglabāt",
"name": "Nosaukuma ignorēšana", "name": "Piesķirt nosaukumu",
"entity_id": "Vienības ID" "entity_id": "Vienības ID"
}, },
"more_info_control": { "more_info_control": {

View File

@ -504,7 +504,12 @@
"leave": "Forlater" "leave": "Forlater"
}, },
"device": { "device": {
"label": "Enhet" "label": "Enhet",
"extra_fields": {
"above": "Over",
"below": "Under",
"for": "Varighet"
}
} }
}, },
"learn_more": "Lær mer om utløsere" "learn_more": "Lær mer om utløsere"

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