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:
- 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
- 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!
-->

View File

@ -104,7 +104,10 @@ gulp.task("compress-static", () => compressStatic(paths.static));
gulp.task("copy-static-demo", (done) => {
// 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
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-ripple": "^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-localize-behavior": "^3.0.1",
"@polymer/app-route": "^3.0.2",
@ -74,7 +74,7 @@
"@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "~2.8.0",
"chartjs-chart-timeline": "^0.3.0",
"codemirror": "^5.45.0",
"codemirror": "^5.49.0",
"cpx": "^1.5.0",
"deep-clone-simple": "^1.1.1",
"es6-object-assign": "^1.1.0",
@ -117,6 +117,7 @@
"@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^3.0.12",
"@types/chromecast-caf-sender": "^1.0.1",
"@types/codemirror": "^0.0.78",
"@types/hls.js": "^0.12.3",
"@types/leaflet": "^1.4.3",
"@types/memoize-one": "4.1.0",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -79,6 +79,18 @@ class StepFlowPickHandler extends LitElement {
`
)}
</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 {
cursor: pointer;
}
p {
text-align: center;
}
`;
}
}

View File

@ -63,19 +63,21 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
color: var(--primary-color);
}
</style>
<hass-subpage header="Home Assistant Cloud">
<hass-subpage header="[[localize('ui.panel.config.cloud.caption')]]">
<div class="content">
<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">
<p>
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!
[[localize('ui.panel.config.cloud.account.thank_you_note')]]
</p>
</div>
<ha-card header="Nabu Casa Account">
<ha-card
header="[[localize('ui.panel.config.cloud.account.nabu_casa_account')]]"
>
<div class="account-row">
<paper-item-body two-line="">
[[cloudStatus.email]]
@ -86,33 +88,37 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
</div>
<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>
<div class="card-actions">
<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"
>Sign out</mwc-button
>[[localize('ui.panel.config.cloud.account.sign_out')]]</mwc-button
>
</div>
</ha-card>
</ha-config-section>
<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">
<p>
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.
[[localize('ui.panel.config.cloud.account.integrations_introduction')]]
</p>
<p>
Check the website for
[[localize('ui.panel.config.cloud.account.integrations_introduction2')]]
<a href="https://www.nabucasa.com" target="_blank"
>all available features</a
>[[localize('ui.panel.config.cloud.account.integrations_link_all_features')]]</a
>.
</p>
</div>
@ -160,7 +166,9 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
}
_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() {
@ -182,7 +190,9 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
_formatSubscription(subInfo) {
if (subInfo === null) {
return "Fetching subscription…";
return this.hass.localize(
"ui.panel.config.cloud.account.fetching_subscription"
);
}
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;
return html`
<ha-card header="Alexa">
<ha-card
header=${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.title"
)}
>
<div class="switch">
<ha-switch
.checked=${alexa_enabled}
@ -39,15 +43,16 @@ export class CloudAlexaPref extends LitElement {
></ha-switch>
</div>
<div class="card-content">
With the Alexa integration for Home Assistant Cloud you'll be able to
control all your Home Assistant devices via any Alexa-enabled device.
${this.hass!.localize("ui.panel.config.cloud.account.alexa.info")}
<ul>
<li>
<a
href="https://skills-store.amazon.com/deeplink/dp/B0772J1QKB?deviceType=app"
target="_blank"
>
Enable the Home Assistant skill for Alexa
${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.enable_ha_skill"
)}
</a>
</li>
<li>
@ -55,37 +60,48 @@ export class CloudAlexaPref extends LitElement {
href="https://www.nabucasa.com/config/amazon_alexa/"
target="_blank"
>
Config documentation
${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.config_documentation"
)}
</a>
</li>
</ul>
<em
>This integration requires an Alexa-enabled device like the Amazon
Echo.</em
>
${alexa_enabled
? 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>
If you enable state reporting, Home Assistant will send
<b>all</b> 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.
${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.info_state_reporting"
)}
</p>
<ha-switch
.checked=${alexa_report_state}
@change=${this._reportToggleChanged}
></ha-switch>
`
: ""}
</div>
<div class="card-actions">
<mwc-button @click=${this._handleSync} .disabled=${this._syncing}>
Sync Entities
${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.sync_entities"
)}
</mwc-button>
<div class="spacer"></div>
<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>
</div>
</ha-card>
@ -97,7 +113,11 @@ export class CloudAlexaPref extends LitElement {
try {
await syncCloudAlexaEntities(this.hass!);
} 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 {
this._syncing = false;
}
@ -122,9 +142,15 @@ export class CloudAlexaPref extends LitElement {
fireEvent(this, "ha-refresh-cloud-status");
} catch (err) {
alert(
`Unable to ${toggle.checked ? "enable" : "disable"} report state. ${
err.message
}`
`${this.hass!.localize(
"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;
}
@ -149,12 +175,22 @@ export class CloudAlexaPref extends LitElement {
.spacer {
flex-grow: 1;
}
h3 {
margin-bottom: 0;
.state-reporting {
display: flex;
margin-top: 1.5em;
}
h3 + p {
.state-reporting + p {
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;
return html`
<ha-card header="Google Assistant">
<ha-card
header=${this.hass!.localize(
"ui.panel.config.cloud.account.google.title"
)}
>
<div class="switch">
<ha-switch
id="google_enabled"
@ -52,16 +56,16 @@ export class CloudGooglePref extends LitElement {
></ha-switch>
</div>
<div class="card-content">
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.
${this.hass!.localize("ui.panel.config.cloud.account.google.info")}
<ul>
<li>
<a
href="https://assistant.google.com/services/a/uid/00000091fd5fb875?hl=en-US"
target="_blank"
>
Activate the Home Assistant skill for Google Assistant
${this.hass!.localize(
"ui.panel.config.cloud.account.google.enable_ha_skill"
)}
</a>
</li>
<li>
@ -69,36 +73,49 @@ export class CloudGooglePref extends LitElement {
href="https://www.nabucasa.com/config/google_assistant/"
target="_blank"
>
Config documentation
${this.hass!.localize(
"ui.panel.config.cloud.account.google.config_documentation"
)}
</a>
</li>
</ul>
<em
>This integration requires a Google Assistant-enabled device like
the Google Home or Android phone.</em
>
${google_enabled
? 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>
If you enable state reporting, Home Assistant will send
<b>all</b> state changes of exposed entities to Google. This
allows you to always see the latest states in the Google app.
${this.hass!.localize(
"ui.panel.config.cloud.account.google.info_state_reporting"
)}
</p>
<ha-switch
.checked=${google_report_state}
@change=${this._reportToggleChanged}
></ha-switch>
<div class="secure_devices">
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.
<h3>
${this.hass!.localize(
"ui.panel.config.cloud.account.google.security_devices"
)}
</h3>
${this.hass!.localize(
"ui.panel.config.cloud.account.google.enter_pin_info"
)}
<paper-input
label="Secure Devices Pin"
label="${this.hass!.localize(
"ui.panel.config.cloud.account.google.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 || ""}
@change="${this._pinChanged}"
></paper-input>
@ -112,11 +129,17 @@ export class CloudGooglePref extends LitElement {
.disabled="${!google_enabled}"
path="cloud/google_actions/sync"
>
Sync entities to Google
${this.hass!.localize(
"ui.panel.config.cloud.account.google.sync_entities"
)}
</ha-call-api-button>
<div class="spacer"></div>
<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>
</div>
</ha-card>
@ -159,7 +182,11 @@ export class CloudGooglePref extends LitElement {
showSaveSuccessToast(this, this.hass!);
fireEvent(this, "ha-refresh-cloud-status");
} 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;
}
}
@ -179,7 +206,7 @@ export class CloudGooglePref extends LitElement {
font-weight: 500;
}
.secure_devices {
padding-top: 16px;
padding-top: 8px;
}
paper-input {
width: 250px;
@ -193,6 +220,25 @@ export class CloudGooglePref extends LitElement {
.spacer {
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) {
return html`
<ha-card header="Remote Control">
<ha-card
header=${this.hass!.localize(
"ui.panel.config.cloud.account.remote.title"
)}
>
<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>
</ha-card>
`;
}
return html`
<ha-card header="Remote Control">
<ha-card
header=${this.hass!.localize(
"ui.panel.config.cloud.account.remote.title"
)}
>
<div class="switch">
<ha-switch
.checked="${remote_connected}"
@ -66,22 +76,33 @@ export class CloudRemotePref extends LitElement {
></ha-switch>
</div>
<div class="card-content">
Home Assistant Cloud provides a secure remote connection to your
instance while away from home. Your instance
${remote_connected ? "is" : "will be"} available at
<a href="https://${remote_domain}" target="_blank">
${this.hass!.localize("ui.panel.config.cloud.account.remote.info")}
${remote_connected
? this.hass!.localize(
"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
>.
</div>
<div class="card-actions">
<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>
${remote_certificate
? html`
<div class="spacer"></div>
<mwc-button @click=${this._openCertInfo}>
Certificate Info
${this.hass!.localize(
"ui.panel.config.cloud.account.remote.certificate_info"
)}
</mwc-button>
`
: ""}
@ -120,6 +141,9 @@ export class CloudRemotePref extends LitElement {
a {
color: var(--primary-color);
}
.break-word {
overflow-wrap: break-word;
}
.switch {
position: absolute;
right: 24px;

View File

@ -51,16 +51,20 @@ export class CloudWebhooks extends LitElement {
protected render() {
return html`
${this.renderStyle()}
<ha-card header="Webhooks">
<ha-card
header=${this.hass!.localize(
"ui.panel.config.cloud.account.webhooks.title"
)}
>
<div class="card-content">
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. ${this._renderBody()}
${this.hass!.localize("ui.panel.config.cloud.account.webhooks.info")}
${this._renderBody()}
<div class="footer">
<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>
</div>
</div>
@ -78,16 +82,33 @@ export class CloudWebhooks extends LitElement {
private _renderBody() {
if (!this.cloudStatus || !this._localHooks || !this._cloudHooks) {
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) {
return html`
<div class="body-text">
Looks like you have no webhooks yet. Get started by configuring a
<a href="/config/integrations">webhook-based integration</a> or by
creating a <a href="/config/automation/new">webhook automation</a>.
${this.hass!.localize(
"ui.panel.config.cloud.account.webhooks.no_hooks_yet"
)}
<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>
`;
}
@ -113,7 +134,9 @@ export class CloudWebhooks extends LitElement {
: this._cloudHooks![entry.webhook_id]
? html`
<mwc-button @click="${this._handleManageButton}">
Manage
${this.hass!.localize(
"ui.panel.config.cloud.account.webhooks.manage"
)}
</mwc-button>
`
: html`
@ -171,7 +194,11 @@ export class CloudWebhooks extends LitElement {
try {
await deleteCloudhook(this.hass!, webhookId!);
} 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;
} finally {
this._progress = this._progress.filter((wid) => wid !== webhookId);

View File

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

View File

@ -40,23 +40,38 @@ class DialogCloudCertificate extends LitElement {
return html`
<ha-paper-dialog with-backdrop>
<h2>Certificate Information</h2>
<h2>
${this.hass!.localize(
"ui.panel.config.cloud.dialog_certificate.certificate_information"
)}
</h2>
<div>
<p>
Certificate expiration date:
${this.hass!.localize(
"ui.panel.config.cloud.dialog_certificate.certificate_expiration_date"
)}
${format_date_time(
new Date(certificateInfo.expire_date),
this.hass!.language
)}<br />
(Will be automatically renewed)
(${this.hass!.localize(
"ui.panel.config.cloud.dialog_certificate.will_be_auto_renewed"
)})
</p>
<p>
Certificate fingerprint: ${certificateInfo.fingerprint}
<p class="break-word">
${this.hass!.localize(
"ui.panel.config.cloud.dialog_certificate.fingerprint"
)}
${certificateInfo.fingerprint}
</p>
</div>
<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>
</ha-paper-dialog>
`;
@ -77,6 +92,9 @@ class DialogCloudCertificate extends LitElement {
ha-paper-dialog {
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}/`;
return html`
<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>
<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
label="${inputLabel}"
value="${cloudhook.cloudhook_url}"
@ -62,13 +72,18 @@ export class DialogManageCloudhook extends LitElement {
<p>
${cloudhook.managed
? html`
This webhook is managed by an integration and cannot be
disabled.
${this.hass!.localize(
"ui.panel.config.cloud.dialog_cloudhook.managed_by_integration"
)}
`
: 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}">
disable it</button
${this.hass!.localize(
"ui.panel.config.cloud.dialog_cloudhook.link_disable_webhook"
)}</button
>.
`}
</p>
@ -76,9 +91,17 @@ export class DialogManageCloudhook extends LitElement {
<div class="paper-dialog-buttons">
<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>
<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>
</ha-paper-dialog>
`;
@ -97,7 +120,13 @@ export class DialogManageCloudhook extends LitElement {
}
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;
}
@ -113,7 +142,9 @@ export class DialogManageCloudhook extends LitElement {
input.setSelectionRange(0, input.value.length);
try {
document.execCommand("copy");
paperInput.label = "COPIED TO CLIPBOARD";
paperInput.label = this.hass!.localize(
"ui.panel.config.cloud.dialog_cloudhook.copied_to_clipboard"
);
} catch (err) {
// Copying failed. Oh no
}

View File

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

View File

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

View File

@ -16,11 +16,15 @@ import "../../ha-config-section";
import { EventsMixin } from "../../../../mixins/events-mixin";
import NavigateMixin from "../../../../mixins/navigate-mixin";
import "../../../../components/ha-icon-next";
import LocalizeMixin from "../../../../mixins/localize-mixin";
/*
* @appliesMixin NavigateMixin
* @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
*/
class CloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) {
class CloudLogin extends LocalizeMixin(
NavigateMixin(EventsMixin(PolymerElement))
) {
static get template() {
return html`
<style include="iron-flex ha-style">
@ -68,31 +72,29 @@ class CloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) {
color: var(--secondary-text-color);
}
</style>
<hass-subpage header="Cloud Login">
<hass-subpage header="[[localize('ui.panel.config.cloud.caption')]]">
<div class="content">
<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">
<p>
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.
[[localize('ui.panel.config.cloud.login.introduction')]]
</p>
<p>
This service is run by our partner
[[localize('ui.panel.config.cloud.login.introduction2')]]
<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.login.introduction2a')]]
</p>
<p>
Home Assistant Cloud is a subscription service with a free one
month trial. No payment information necessary.
[[localize('ui.panel.config.cloud.login.introduction3')]]
</p>
<p>
<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>
</div>
@ -101,44 +103,46 @@ class CloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) {
<div class="card-content flash-msg">
[[flashMessage]]
<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>
</div>
</ha-card>
<ha-card header="Sign in">
<ha-card
header="[[localize('ui.panel.config.cloud.login.sign_in')]]"
>
<div class="card-content">
<div class="error" hidden$="[[!_error]]">[[_error]]</div>
<paper-input
label="Email"
label="[[localize('ui.panel.config.cloud.login.email')]]"
id="email"
type="email"
value="{{email}}"
on-keydown="_keyDown"
error-message="Invalid email"
error-message="[[localize('ui.panel.config.cloud.login.email_error_msg')]]"
></paper-input>
<paper-input
id="password"
label="Password"
label="[[localize('ui.panel.config.cloud.login.password')]]"
value="{{_password}}"
type="password"
on-keydown="_keyDown"
error-message="Passwords are at least 8 characters"
error-message="[[localize('ui.panel.config.cloud.login.password_error_msg')]]"
></paper-input>
</div>
<div class="card-actions">
<ha-progress-button
on-click="_handleLogin"
progress="[[_requestInProgress]]"
>Sign in</ha-progress-button
>[[localize('ui.panel.config.cloud.login.sign_in')]]</ha-progress-button
>
<button
class="link"
hidden="[[_requestInProgress]]"
on-click="_handleForgotPassword"
>
forgot password?
[[localize('ui.panel.config.cloud.login.forgot_password')]]
</button>
</div>
</ha-card>
@ -146,8 +150,10 @@ class CloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) {
<ha-card>
<paper-item on-click="_handleRegister">
<paper-item-body two-line="">
Start your free 1 month trial
<div secondary="">No payment information necessary</div>
[[localize('ui.panel.config.cloud.login.start_trial')]]
<div secondary="">
[[localize('ui.panel.config.cloud.login.trial_info')]]
</div>
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
@ -251,7 +257,9 @@ class CloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) {
const errCode = err && err.body && err.body.code;
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");
return;
}
@ -265,7 +273,8 @@ class CloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) {
};
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);

View File

@ -8,11 +8,13 @@ import "../../../../layouts/hass-subpage";
import "../../../../resources/ha-style";
import "../../ha-config-section";
import { EventsMixin } from "../../../../mixins/events-mixin";
import LocalizeMixin from "../../../../mixins/localize-mixin";
/*
* @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
*/
class CloudRegister extends EventsMixin(PolymerElement) {
class CloudRegister extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex ha-style">
@ -48,47 +50,47 @@ class CloudRegister extends EventsMixin(PolymerElement) {
display: none;
}
</style>
<hass-subpage header="Register Account">
<hass-subpage header="[[localize('ui.panel.config.cloud.register.title')]]">
<div class="content">
<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">
<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>
The trial will give you access to all the benefits of Home Assistant Cloud, including:
[[localize('ui.panel.config.cloud.register.information2')]]
</p>
<ul>
<li>Control of Home Assistant away from home</li>
<li>Integration with Google Assistant</li>
<li>Integration with Amazon Alexa</li>
<li>Easy integration with webhook-based apps like OwnTracks</li>
<li>[[localize('ui.panel.config.cloud.register.feature_remote_control')]]</li>
<li>[[localize('ui.panel.config.cloud.register.feature_google_home')]]</li>
<li>[[localize('ui.panel.config.cloud.register.feature_amazon_alexa')]]</li>
<li>[[localize('ui.panel.config.cloud.register.feature_webhook_apps')]]</li>
</ul>
<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>
By registering an account you agree to the following terms and conditions.
[[localize('ui.panel.config.cloud.register.information4')]]
</p><ul>
<li><a href="https://home-assistant.io/tos/" target="_blank">Terms and Conditions</a></li>
<li><a href="https://home-assistant.io/privacy/" target="_blank">Privacy Policy</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">[[localize('ui.panel.config.cloud.register.link_privacy_policy')]]</a></li>
</ul>
</p>
</div>
<ha-card header="Create Account">
<ha-card header="[[localize('ui.panel.config.cloud.register.create_account')]]">
<div class="card-content">
<div class="header">
<div class="error" hidden$="[[!_error]]">[[_error]]</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 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 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="[[localize('ui.panel.config.cloud.register.password_error_msg')]]"></paper-input>
</div>
<div class="card-actions">
<ha-progress-button on-click="_handleRegister" progress="[[_requestInProgress]]">Start trial</ha-progress-button>
<button class="link" hidden="[[_requestInProgress]]" on-click="_handleResendVerifyEmail">Resend confirmation email</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">[[localize('ui.panel.config.cloud.register.resend_confirmation_email')]]</button>
</div>
</ha-card>
</ha-config-section>
@ -211,8 +213,9 @@ class CloudRegister extends EventsMixin(PolymerElement) {
_password: "",
});
this.fire("cloud-done", {
flashMessage:
"Account created! Check your email for instructions on how to activate your account.",
flashMessage: this.hass.localize(
"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 { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../../components/ha-card";
import "../../../../layouts/hass-subpage";
import { EventsMixin } from "../../../../mixins/events-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 {
subscribeDeviceRegistry,
updateDeviceRegistryEntry,
} from "../../../../data/device_registry";
import { subscribeAreaRegistry } from "../../../../data/area_registry";
import { updateDeviceRegistryEntry } from "../../../../data/device_registry";
import {
loadDeviceRegistryDetailDialog,
showDeviceRegistryDetailDialog,
} 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
*/
@ -37,10 +19,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
static get template() {
return html`
<style>
:host(:not([narrow])) .device-entities {
max-height: 225px;
overflow: auto;
}
ha-card {
flex: 1 0 100%;
padding-bottom: 10px;
@ -70,11 +48,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
.extra-info {
margin-top: 8px;
}
paper-icon-item {
cursor: pointer;
padding-top: 4px;
padding-bottom: 4px;
}
.manuf,
.entity-id,
.area {
@ -82,15 +55,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
}
</style>
<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="info">
<div class="model">[[device.model]]</div>
@ -122,27 +86,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
</div>
</template>
</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>
`;
}
@ -152,14 +95,11 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
device: Object,
devices: Array,
areas: Array,
entities: Array,
hass: Object,
narrow: {
type: Boolean,
reflectToAttribute: true,
},
hideSettings: { type: Boolean, value: false },
hideEntities: { type: Boolean, value: false },
_childDevices: {
type: Array,
computed: "_computeChildDevices(device, devices)",
@ -172,30 +112,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
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) {
if (!areas || !device || !device.area_id) {
return "No Area";
@ -210,30 +126,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
.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) {
return device.name_by_user || device.name;
}

View File

@ -13,6 +13,7 @@ import "../../../layouts/hass-subpage";
import "../../../layouts/hass-error-screen";
import "../ha-config-section";
import "./device-detail/ha-device-card";
import "./device-detail/ha-device-triggers-card";
import "./device-detail/ha-device-conditions-card";
import "./device-detail/ha-device-actions-card";
@ -144,9 +145,6 @@ export class HaConfigDevicePage extends LitElement {
.areas=${this.areas}
.devices=${this.devices}
.device=${device}
.entities=${this.entities}
hide-settings
hide-entities
></ha-device-card>
${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 "../../../resources/ha-style";
import "../../../components/ha-icon-next";
import "../ha-config-section";
import memoizeOne from "memoize-one";
import "./ha-devices-data-table";
import {
LitElement,
@ -21,33 +7,14 @@ import {
TemplateResult,
property,
customElement,
CSSResult,
css,
} from "lit-element";
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 { 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";
interface DeviceRowData extends DeviceRegistryEntry {
device?: DeviceRowData;
area?: string;
integration?: string;
battery_entity?: string;
}
interface DeviceEntityLookup {
[deviceId: string]: EntityRegistryEntry[];
}
@customElement("ha-config-devices-dashboard")
export class HaConfigDeviceDashboard extends LitElement {
@ -59,234 +26,35 @@ export class HaConfigDeviceDashboard extends LitElement {
@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): 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 {
return html`
<hass-subpage
header=${this.hass.localize("ui.panel.config.devices.caption")}
>
<ha-data-table
.columns=${this._columns(this.narrow)}
.data=${this._devices(
this.devices,
this.entries,
this.entities,
this.areas,
this.domain,
this.hass.localize
).map((device: DeviceRowData) => {
// We don't need a lot of this data for mobile view, but kept it for filtering...
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>
<div class="content">
<ha-devices-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.devices=${this.devices}
.entries=${this.entries}
.entities=${this.entities}
.areas=${this.areas}
.domain=${this.domain}
></ha-devices-data-table>
</div>
</hass-subpage>
`;
}
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);
static get styles(): CSSResult {
return css`
.content {
padding: 4px;
}
}
return undefined;
}
private _handleRowClicked(ev: CustomEvent) {
const deviceId = (ev.detail as RowClickedEvent).id;
navigate(this, `/config/devices/device/${deviceId}`);
ha-devices-data-table {
width: 100%;
}
`;
}
}

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,
property,
} 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 {
@ -18,7 +15,7 @@ import {
} from "../../../data/entity_registry";
import "../../../layouts/hass-subpage";
import "../../../layouts/hass-loading-screen";
import "../../../components/ha-card";
import "../../../components/data-table/ha-data-table";
import "../../../components/ha-icon";
import "../../../components/ha-switch";
import { domainIcon } from "../../../common/entity/domain_icon";
@ -30,11 +27,14 @@ import {
loadEntityRegistryDetailDialog,
} from "./show-dialog-entity-registry-detail";
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
import { HaSwitch } from "../../../components/ha-switch";
import memoize from "memoize-one";
// tslint:disable-next-line
import {
DataTableColumnContainer,
RowClickedEvent,
} from "../../../components/data-table/ha-data-table";
class HaConfigEntityRegistry extends LitElement {
@property() public hass!: HomeAssistant;
@ -43,11 +43,76 @@ class HaConfigEntityRegistry extends LitElement {
@property() private _showDisabled = false;
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(
(entities: EntityRegistryEntry[], showDisabled: boolean) =>
showDisabled
(showDisabled
? entities
: 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() {
@ -89,56 +154,21 @@ class HaConfigEntityRegistry extends LitElement {
"ui.panel.config.entity_registry.picker.integrations_page"
)}
</a>
</span>
<ha-card>
<paper-item>
<ha-switch
?checked=${this._showDisabled}
@change=${this._showDisabledChanged}
>${this.hass.localize(
"ui.panel.config.entity_registry.picker.show_disabled"
)}</ha-switch
></paper-item
<ha-switch
?checked=${this._showDisabled}
@change=${this._showDisabledChanged}
>${this.hass.localize(
"ui.panel.config.entity_registry.picker.show_disabled"
)}</ha-switch
>
${this._filteredEntities(this._entities, this._showDisabled).map(
(entry) => {
const state = this.hass!.states[entry.entity_id];
return html`
<paper-icon-item
@click=${this._openEditEntry}
.entry=${entry}
class=${classMap({ "disabled-entry": !!entry.disabled_by })}
>
<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>
</span>
<ha-data-table
.columns=${this._columns(this.hass.language)}
.data=${this._filteredEntities(this._entities, this._showDisabled)}
@row-click=${this._openEditEntry}
id="entity_id"
>
</ha-data-table>
</ha-config-section>
</hass-subpage>
`;
@ -155,9 +185,7 @@ class HaConfigEntityRegistry extends LitElement {
this._unsubEntities = subscribeEntityRegistry(
this.hass.connection,
(entities) => {
this._entities = entities.sort((ent1, ent2) =>
compare(ent1.entity_id, ent2.entity_id)
);
this._entities = entities;
}
);
}
@ -167,8 +195,14 @@ class HaConfigEntityRegistry extends LitElement {
this._showDisabled = (ev.target as HaSwitch).checked;
}
private _openEditEntry(ev: MouseEvent): void {
const entry = (ev.currentTarget! as any).entry;
private _openEditEntry(ev: CustomEvent): void {
const entryId = (ev.detail as RowClickedEvent).id;
const entry = this._entities!.find(
(entity) => entity.entity_id === entryId
);
if (!entry) {
return;
}
showEntityRegistryDetailDialog(this, {
entry,
});
@ -179,23 +213,12 @@ class HaConfigEntityRegistry extends LitElement {
a {
color: var(--primary-color);
}
ha-card {
ha-data-table {
margin-bottom: 24px;
direction: ltr;
margin-top: 0px;
}
paper-icon-item {
cursor: pointer;
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);
ha-switch {
margin-top: 16px;
}
`;
}

View File

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

View File

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

View File

@ -2,29 +2,49 @@ import { h, Component } from "preact";
import "../../../../components/device/ha-device-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> {
private _origCondition;
constructor() {
super();
this.devicePicked = this.devicePicked.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) {
this.setState({ device_id: ev.target.value });
this.setState({ ...this.state, device_id: ev.target.value });
}
public deviceConditionPicked(ev) {
const deviceCondition = ev.target.value;
this.props.onChange(this.props.index, deviceCondition);
let condition = ev.target.value;
if (
this._origCondition &&
deviceAutomationsEqual(this._origCondition, condition)
) {
condition = this._origCondition;
}
this.props.onChange(this.props.index, condition);
}
/* eslint-disable camelcase */
public render({ condition, hass }, { device_id }) {
public render({ condition, hass }, { device_id, capabilities }) {
if (device_id === undefined) {
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 (
<div>
@ -41,9 +61,64 @@ export default class DeviceCondition extends Component<any, any> {
hass={hass}
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>
);
}
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 = {

View File

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

View File

@ -8,6 +8,7 @@ import ConditionAction from "./condition";
import DelayAction from "./delay";
import DeviceAction from "./device";
import EventAction from "./event";
import SceneAction from "./scene";
import WaitAction from "./wait";
const TYPES = {
@ -17,6 +18,7 @@ const TYPES = {
condition: ConditionAction,
event: EventAction,
device_id: DeviceAction,
scene: SceneAction,
};
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 yaml from "js-yaml";
import "../../../components/ha-textarea";
import "../../../components/ha-code-editor";
const isEmpty = (obj: object) => {
for (const key in obj) {
@ -34,7 +34,7 @@ export default class YAMLTextArea extends Component<any, any> {
}
public onChange(ev) {
const value = ev.target.value;
const value = ev.detail.value;
let parsed;
let isValid = true;
@ -64,17 +64,17 @@ export default class YAMLTextArea extends Component<any, any> {
minWidth: 300,
width: "100%",
};
if (!isValid) {
style.border = "1px solid red";
}
return (
<ha-textarea
label={label}
value={value}
style={style}
onvalue-changed={this.onChange}
dir="ltr"
/>
<div>
<p>{label}</p>
<ha-code-editor
mode="yaml"
style={style}
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-input/paper-input";
import "@material/mwc-button";
import memoizeOne from "memoize-one";
import "../../../components/dialog/ha-paper-dialog";
import "../../../components/entity/ha-entities-picker";
import "../../../components/user/ha-user-picker";
import { PersonDetailDialogParams } from "./show-dialog-person-detail";
@ -29,6 +29,13 @@ class DialogPersonDetail extends LitElement {
@property() private _params?: PersonDetailDialogParams;
@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> {
this._params = params;
this._error = undefined;
@ -55,7 +62,11 @@ class DialogPersonDetail extends LitElement {
opened
@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>
${this._error
? html`
@ -66,34 +77,72 @@ class DialogPersonDetail extends LitElement {
<paper-input
.value=${this._name}
@value-changed=${this._nameChanged}
label="Name"
error-message="Name is required"
label="${this.hass!.localize(
"ui.panel.config.person.detail.name"
)}"
error-message="${this.hass!.localize(
"ui.panel.config.person.detail.name_error_msg"
)}"
.invalid=${nameInvalid}
></paper-input>
<ha-user-picker
label="Linked User"
label="${this.hass!.localize(
"ui.panel.config.person.detail.linked_user"
)}"
.hass=${this.hass}
.value=${this._userId}
.users=${this._params.users}
@value-changed=${this._userChanged}
></ha-user-picker>
<p>
${this.hass.localize(
"ui.panel.config.person.detail.device_tracker_intro"
)}
</p>
<ha-entities-picker
.hass=${this.hass}
.value=${this._deviceTrackers}
domain-filter="device_tracker"
.pickedEntityLabel=${this.hass.localize(
"ui.panel.config.person.detail.device_tracker_picked"
)}
.pickEntityLabel=${this.hass.localize(
"ui.panel.config.person.detail.device_tracker_pick"
)}
@value-changed=${this._deviceTrackersChanged}
></ha-entities-picker>
${this._deviceTrackersAvailable(this.hass)
? html`
<p>
${this.hass.localize(
"ui.panel.config.person.detail.device_tracker_intro"
)}
</p>
<ha-entities-picker
.hass=${this.hass}
.value=${this._deviceTrackers}
domain-filter="device_tracker"
.pickedEntityLabel=${this.hass.localize(
"ui.panel.config.person.detail.device_tracker_picked"
)}
.pickEntityLabel=${this.hass.localize(
"ui.panel.config.person.detail.device_tracker_pick"
)}
@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>
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
@ -104,7 +153,7 @@ class DialogPersonDetail extends LitElement {
@click="${this._deleteEntry}"
.disabled=${this._submitting}
>
DELETE
${this.hass!.localize("ui.panel.config.person.detail.delete")}
</mwc-button>
`
: html``}
@ -112,13 +161,19 @@ class DialogPersonDetail extends LitElement {
@click="${this._updateEntry}"
.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>
</div>
</ha-paper-dialog>
`;
}
private _closeDialog() {
this._params = undefined;
}
private _nameChanged(ev: PolymerChangedEvent<string>) {
this._error = undefined;
this._name = ev.detail.value;

View File

@ -55,17 +55,21 @@ class HaConfigPerson extends LitElement {
<hass-loading-screen></hass-loading-screen>
`;
}
const hass = this.hass;
return html`
<hass-subpage header="Persons">
<hass-subpage header=${hass.localize("ui.panel.config.person.caption")}>
<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">
Here you can define each person of interest in Home Assistant.
${hass.localize("ui.panel.config.person.introduction")}
${this._configItems.length > 0
? html`
<p>
Note: persons configured via configuration.yaml cannot be
edited via the UI.
${hass.localize(
"ui.panel.config.person.note_about_persons_configured_in_yaml"
)}
</p>
`
: ""}
@ -83,9 +87,13 @@ class HaConfigPerson extends LitElement {
${this._storageItems.length === 0
? html`
<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}>
CREATE PERSON</mwc-button
${hass.localize(
"ui.panel.config.person.create_person"
)}</mwc-button
>
</div>
`
@ -112,7 +120,7 @@ class HaConfigPerson extends LitElement {
<ha-fab
?is-wide=${this.isWide}
icon="hass:plus"
title="Add Person"
title="${hass.localize("ui.panel.config.person.add_person")}"
@click=${this._createPerson}
></ha-fab>
`;
@ -180,9 +188,11 @@ class HaConfigPerson extends LitElement {
},
removeEntry: async () => {
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;
}

View File

@ -100,7 +100,7 @@ class HaScriptEditor extends LocalizeMixin(NavigateMixin(PolymerElement)) {
<ha-paper-icon-button-arrow-prev
on-click="backTapped"
></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]]">
<paper-icon-button
icon="hass:delete"
@ -120,7 +120,7 @@ class HaScriptEditor extends LocalizeMixin(NavigateMixin(PolymerElement)) {
is-wide$="[[isWide]]"
dirty$="[[dirty]]"
icon="hass:content-save"
title="Save"
title="[[localize('ui.common.save')]]"
on-click="saveScript"
rtl$="[[rtl]]"
></ha-fab>
@ -232,7 +232,11 @@ class HaScriptEditor extends LocalizeMixin(NavigateMixin(PolymerElement)) {
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();
}
);
@ -244,7 +248,7 @@ class HaScriptEditor extends LocalizeMixin(NavigateMixin(PolymerElement)) {
}
this.dirty = false;
this.config = {
alias: "New Script",
alias: this.hass.localize("ui.panel.config.script.editor.default_name"),
sequence: [{ service: "", data: {} }],
};
this._updateComponent();
@ -254,7 +258,9 @@ class HaScriptEditor extends LocalizeMixin(NavigateMixin(PolymerElement)) {
if (
this.dirty &&
// 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;
}
@ -281,7 +287,11 @@ class HaScriptEditor extends LocalizeMixin(NavigateMixin(PolymerElement)) {
}
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;
}
await deleteScript(this.hass, computeObjectId(this.script.entity_id));
@ -307,8 +317,14 @@ class HaScriptEditor extends LocalizeMixin(NavigateMixin(PolymerElement)) {
);
}
computeName(script) {
return script && computeStateName(script);
computeHeader(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) {

View File

@ -38,22 +38,32 @@ class HaScriptPicker extends LitElement {
.header=${this.hass.localize("ui.panel.config.script.caption")}
>
<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">
The script editor allows you to create and edit scripts. Please read
<a
href="https://home-assistant.io/docs/scripts/editor/"
target="_blank"
>the instructions</a
>
to make sure that you have configured Home Assistant correctly.
${this.hass.localize("ui.panel.config.script.picker.introduction")}
<p>
<a
href="https://home-assistant.io/docs/scripts/editor/"
target="_blank"
>
${this.hass.localize(
"ui.panel.config.script.picker.learn_more"
)}
</a>
</p>
</div>
<ha-card header="Pick script to edit">
<ha-card>
${this.scripts.length === 0
? html`
<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>
`
: this.scripts.map(
@ -85,7 +95,9 @@ class HaScriptPicker extends LitElement {
slot="fab"
?is-wide=${this.isWide}
icon="hass:plus"
title="Add Script"
title="${this.hass.localize(
"ui.panel.config.script.picker.add_script"
)}"
?rtl=${computeRTL(this.hass)}
></ha-fab>
</a>
@ -97,7 +109,11 @@ class HaScriptPicker extends LitElement {
const script = ev.currentTarget.script as HassEntity;
await triggerScript(this.hass, script.entity_id);
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="">
[[_computeGroup(localize, user)]]
<template is="dom-if" if="[[user.system_generated]]">
- System Generated
-
[[localize('ui.panel.config.users.picker.system_generated')]]
</template>
</div>
</paper-item-body>

View File

@ -52,15 +52,15 @@ class HaUserEditor extends LitElement {
<ha-card .header=${this._name}>
<table class="card-content">
<tr>
<td>ID</td>
<td>${hass.localize("ui.panel.config.users.editor.id")}</td>
<td>${user.id}</td>
</tr>
<tr>
<td>Owner</td>
<td>${hass.localize("ui.panel.config.users.editor.owner")}</td>
<td>${user.is_owner}</td>
</tr>
<tr>
<td>Group</td>
<td>${hass.localize("ui.panel.config.users.editor.group")}</td>
<td>
<select
@change=${this._handleGroupChange}
@ -92,11 +92,15 @@ class HaUserEditor extends LitElement {
: ""}
<tr>
<td>Active</td>
<td>${hass.localize("ui.panel.config.users.editor.active")}</td>
<td>${user.is_active}</td>
</tr>
<tr>
<td>System generated</td>
<td>
${hass.localize(
"ui.panel.config.users.editor.system_generated"
)}
</td>
<td>${user.system_generated}</td>
</tr>
</table>
@ -114,7 +118,9 @@ class HaUserEditor extends LitElement {
</mwc-button>
${user.system_generated
? html`
Unable to remove system generated users.
${hass.localize(
"ui.panel.config.users.editor.system_generated_users_not_removable"
)}
`
: ""}
</div>
@ -124,12 +130,19 @@ class HaUserEditor extends LitElement {
}
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> {
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) {
return;
}
@ -140,7 +153,11 @@ class HaUserEditor extends LitElement {
});
fireEvent(this, "reload-users");
} 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!);
fireEvent(this, "reload-users");
} 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];
}
}
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();
return;
}

View File

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

View File

@ -85,7 +85,11 @@ export class ZHAClusterAttributes extends LitElement {
return html`
<ha-config-section .isWide="${this.isWide}">
<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
class="toggle-help-icon"
@click="${this._onHelpTap}"
@ -93,12 +97,18 @@ export class ZHAClusterAttributes extends LitElement {
>
</paper-icon-button>
</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">
<div class="attribute-picker">
<paper-dropdown-menu
label="Attributes of the selected cluster"
label="${this.hass!.localize(
"ui.panel.config.zha.cluster_attributes.attributes_of_cluster"
)}"
class="flex"
>
<paper-listbox
@ -122,7 +132,9 @@ export class ZHAClusterAttributes extends LitElement {
${this.showHelp
? html`
<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>
`
: ""}
@ -138,30 +150,40 @@ export class ZHAClusterAttributes extends LitElement {
return html`
<div class="input-text">
<paper-input
label="Value"
label="${this.hass!.localize("ui.panel.config.zha.common.value")}"
type="string"
.value="${this._attributeValue}"
@value-changed="${this._onAttributeValueChanged}"
placeholder="Value"
placeholder="${this.hass!.localize(
"ui.panel.config.zha.common.value"
)}"
></paper-input>
</div>
<div class="input-text">
<paper-input
label="Manufacturer code override"
label="${this.hass!.localize(
"ui.panel.config.zha.common.manufacturer_code_override"
)}"
type="number"
.value="${this._manufacturerCodeOverride}"
@value-changed="${this._onManufacturerCodeOverrideChanged}"
placeholder="Value"
placeholder="${this.hass!.localize(
"ui.panel.config.zha.common.value"
)}"
></paper-input>
</div>
<div class="card-actions">
<mwc-button @click="${this._onGetZigbeeAttributeClick}"
>Get Zigbee Attribute</mwc-button
>
<mwc-button @click="${this._onGetZigbeeAttributeClick}">
${this.hass!.localize(
"ui.panel.config.zha.cluster_attributes.get_zigbee_attribute"
)}
</mwc-button>
${this.showHelp
? html`
<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>
`
: ""}
@ -170,8 +192,11 @@ export class ZHAClusterAttributes extends LitElement {
domain="zha"
service="set_zigbee_cluster_attribute"
.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
? html`
<ha-service-description

View File

@ -78,7 +78,11 @@ export class ZHAClusterCommands extends LitElement {
return html`
<ha-config-section .isWide="${this.isWide}">
<div class="sectionHeader" slot="header">
<span>Cluster Commands</span>
<span>
${this.hass!.localize(
"ui.panel.config.zha.cluster_commands.header"
)}
</span>
<paper-icon-button
class="toggle-help-icon"
@click="${this._onHelpTap}"
@ -86,12 +90,18 @@ export class ZHAClusterCommands extends LitElement {
>
</paper-icon-button>
</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">
<div class="command-picker">
<paper-dropdown-menu
label="Commands of the selected cluster"
label="${this.hass!.localize(
"ui.panel.config.zha.cluster_commands.commands_of_cluster"
)}"
class="flex"
>
<paper-listbox
@ -114,18 +124,26 @@ export class ZHAClusterCommands extends LitElement {
</div>
${this._showHelp
? 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
? html`
<div class="input-text">
<paper-input
label="Manufacturer code override"
label="${this.hass!.localize(
"ui.panel.config.zha.common.manufacturer_code_override"
)}"
type="number"
.value="${this._manufacturerCodeOverride}"
@value-changed="${this._onManufacturerCodeOverrideChanged}"
placeholder="Value"
placeholder="${this.hass!.localize(
"ui.panel.config.zha.common.value"
)}"
></paper-input>
</div>
<div class="card-actions">
@ -134,8 +152,11 @@ export class ZHAClusterCommands extends LitElement {
domain="zha"
service="issue_zigbee_cluster_command"
.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
? html`
<ha-service-description

View File

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

View File

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

View File

@ -41,19 +41,27 @@ export class ZHANetwork extends LitElement {
return html`
<ha-config-section .isWide="${this.isWide}">
<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
class="toggle-help-icon"
@click="${this._onHelpTap}"
icon="hass:help-circle"
></paper-icon-button>
</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">
<div class="card-actions">
<mwc-button @click=${this._onAddDevicesClick}>
Add Devices
${this.hass!.localize("ui.panel.config.zha.common.add_devices")}
</mwc-button>
${this._showHelp
? html`

View File

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

View File

@ -44,6 +44,11 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) {
margin-top: 24px;
}
.sectionHeader {
position: relative;
padding-right: 40px;
}
.node-info {
margin-left: 16px;
}
@ -77,7 +82,7 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) {
.toggle-help-icon {
position: absolute;
top: 6px;
top: -6px;
right: 0;
color: var(--primary-color);
}
@ -102,7 +107,7 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) {
<!-- Node card -->
<ha-config-section is-wide="[[isWide]]">
<div style="position: relative" slot="header">
<div class="sectionHeader" slot="header">
<span>Z-Wave Node Management</span>
<paper-icon-button
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 { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import LocalizeMixin from "../../../mixins/localize-mixin";
import { EventsMixin } from "../../../mixins/events-mixin";
import isPwa from "../../../common/config/is_pwa";
@ -11,7 +12,7 @@ import "../../../components/ha-card";
let registeredDialog = false;
class OzwLog extends EventsMixin(PolymerElement) {
class OzwLog extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex ha-style">
@ -32,8 +33,13 @@ class OzwLog extends EventsMixin(PolymerElement) {
</style>
<ha-config-section is-wide="[[isWide]]">
<span slot="header">OZW Log</span>
<ha-card>
<span slot="header">
[[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">
<paper-input label="Number of last log lines." type="number" min="0" max="1000" step="10" value="{{numLogLines}}">
</paper-input>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,10 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
}
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;

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 { LovelaceCard } from "../types";
@ -48,12 +48,31 @@ export abstract class HuiStackCard extends LitElement implements LovelaceCard {
return html`
${this.renderStyle()}
${this._config.title
? html`
<div class="card-header">${this._config.title}</div>
`
: ""}
<div id="root">${this._cards}</div>
`;
}
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) {
const element = createCardElement(cardConfig) as LovelaceCard;
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 { HomeAssistant } from "../../../types";
const EXCLUDED_DOMAINS = ["zone"];
const EXCLUDED_DOMAINS = ["zone", "persistent_notification"];
const addFromAction = (entities: Set<string>, actionConfig: ActionConfig) => {
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 { LovelaceCardEditor } from "../../types";
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.
// 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 { EntityConfig } from "../../entity-rows/types";
@ -43,7 +44,7 @@ export interface UIConfigChangedEvent extends Event {
@customElement("hui-card-editor")
export class HuiCardEditor extends LitElement {
@property() public hass?: HomeAssistant;
@property() public hass!: HomeAssistant;
@property() private _yaml?: string;
@property() private _config?: LovelaceCardConfig;
@ -64,12 +65,6 @@ export class HuiCardEditor extends LitElement {
try {
this._config = yaml.safeLoad(this.yaml);
this._updateConfigElement();
setTimeout(() => {
if (this._yamlEditor) {
this._yamlEditor.codemirror.refresh();
}
fireEvent(this as HTMLElement, "iron-resize");
}, 1);
this._error = undefined;
} catch (err) {
this._error = err.message;
@ -93,14 +88,19 @@ export class HuiCardEditor extends LitElement {
return this._error !== undefined;
}
private get _yamlEditor(): HuiYamlEditor {
return this.shadowRoot!.querySelector("hui-yaml-editor")!;
private get _yamlEditor(): HaCodeEditor {
return this.shadowRoot!.querySelector("ha-code-editor")! as HaCodeEditor;
}
public toggleMode() {
this._GUImode = !this._GUImode;
}
public connectedCallback() {
super.connectedCallback();
this._refreshYamlEditor();
}
protected render(): TemplateResult {
return html`
<div class="wrapper">
@ -120,11 +120,14 @@ export class HuiCardEditor extends LitElement {
`
: html`
<div class="yaml-editor">
<hui-yaml-editor
.hass=${this.hass}
<ha-code-editor
mode="yaml"
autofocus
.value=${this.yaml}
@yaml-changed=${this._handleYAMLChanged}
></hui-yaml-editor>
.error=${this._error}
.rtl=${computeRTL(this.hass)}
@value-changed=${this._handleYAMLChanged}
></ha-code-editor>
</div>
`}
${this._error
@ -145,9 +148,12 @@ export class HuiCardEditor extends LitElement {
<mwc-button
@click=${this.toggleMode}
?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>
</div>
</div>
@ -160,13 +166,25 @@ export class HuiCardEditor extends LitElement {
if (changedProperties.has("_GUImode")) {
if (this._GUImode === false) {
// Refresh code editor when switching to yaml mode
this._yamlEditor.codemirror.refresh();
this._yamlEditor.codemirror.focus();
this._refreshYamlEditor(true);
}
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) {
ev.stopPropagation();
const config = ev.detail.config;

View File

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

View File

@ -62,10 +62,21 @@ export class HuiDialogEditCard extends LitElement {
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`
<ha-paper-dialog with-backdrop opened modal>
<h2>
${this.hass!.localize("ui.panel.lovelace.editor.edit_card.header")}
${heading}
</h2>
<paper-dialog-scrollable>
${this._cardConfig === undefined

View File

@ -101,7 +101,7 @@ export class HuiAlarmPanelCardEditor extends LitElement
})}
<paper-dropdown-menu
.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}"
>

View File

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

View File

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

View File

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

View File

@ -236,7 +236,20 @@ class HUIRoot extends LitElement {
>
${this.lovelace!.config.views.map(
(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
? html`
<ha-paper-icon-button-arrow-prev
@ -392,6 +405,9 @@ class HUIRoot extends LitElement {
paper-item {
cursor: pointer;
}
.hide-tab {
display: none;
}
`,
];
}

View File

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

View File

@ -23,7 +23,9 @@ class AdvancedModeCard extends LitElement {
return html`
<ha-card>
<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
.checked=${this.coreUserData && this.coreUserData.showAdvanced}
.disabled=${this.coreUserData === undefined}
@ -31,10 +33,7 @@ class AdvancedModeCard extends LitElement {
></ha-switch>
</div>
<div class="card-content">
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.
${this.hass.localize("ui.panel.profile.advanced_mode.description")}
</div>
</ha-card>
`;

View File

@ -28,7 +28,7 @@ class HaPushNotificationsRow extends LocalizeMixin(PolymerElement) {
<span slot="description">
[[_description(_platformLoaded, _pushSupported)]]
<a
href="https://www.home-assistant.io/integrations/notify.html5/"
href="https://www.home-assistant.io/integrations/html5"
target="_blank"
>[[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);
--error-state-color: #db4437;
/* states and badges */
--state-icon-color: #44739e;
--state-icon-active-color: #FDD835;

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

@ -566,11 +566,20 @@
"zha_device_info": {
"manuf": "by {manufacturer}",
"no_area": "No Area",
"buttons": {
"add": "Add Devices",
"remove": "Remove Device",
"reconfigure": "Reconfigure Device"
},
"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.",
"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": {
"device_name_placeholder": "User given name",
"area_picker_label": "Area",
@ -597,7 +606,8 @@
},
"notification_toast": {
"service_call_failed": "Failed to call service {service}.",
"connection_lost": "Connection lost. Reconnecting…"
"connection_lost": "Connection lost. Reconnecting…",
"triggered": "Triggered {name}"
},
"sidebar": {
"external_app_configuration": "App Configuration"
@ -606,6 +616,11 @@
"config": {
"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.",
"common": {
"editor": {
"confirm_unsaved": "You have unsaved changes. Are you sure you want to leave?"
}
},
"area_registry": {
"caption": "Area Registry",
"description": "Overview of all areas in your home.",
@ -815,7 +830,12 @@
"label": "And"
},
"device": {
"label": "Device"
"label": "Device",
"extra_fields": {
"above": "Above",
"below": "Below",
"for": "Duration"
}
},
"numeric_state": {
"label": "[%key:ui::panel::config::automation::editor::triggers::type::numeric_state::label%]",
@ -889,6 +909,9 @@
},
"device_id": {
"label": "Device"
},
"scene": {
"label": "Activate scene"
}
}
}
@ -896,13 +919,175 @@
},
"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": {
"caption": "Home Assistant Cloud",
"description_login": "Logged in as {email}",
"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": {
"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.",
"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",
"show_disabled": "Show disabled entities"
"show_disabled": "Show disabled entities",
"headers": {
"name": "Name",
"entity_id": "Entity ID",
"integration": "Integration",
"enabled": "Enabled"
}
},
"editor": {
"unavailable": "This entity is not currently available.",
@ -944,11 +1135,27 @@
"person": {
"caption": "Persons",
"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": {
"new_person": "New Person",
"name": "Name",
"name_error_msg": "Name is required",
"linked_user": "Linked User",
"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_pick": "Pick device to track"
"device_tracker_pick": "Pick device to track",
"delete": "Delete",
"create": "Create",
"update": "Update"
}
},
"integrations": {
@ -957,6 +1164,9 @@
"discovered": "Discovered",
"configured": "Configured",
"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",
"none": "Nothing configured yet",
"config_entry": {
@ -982,7 +1192,8 @@
"caption": "Users",
"description": "Manage users",
"picker": {
"title": "Users"
"title": "Users",
"system_generated": "System generated"
},
"editor": {
"caption": "View user",
@ -990,7 +1201,18 @@
"change_password": "Change password",
"activate_user": "Activate 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": {
"caption": "Add user",
@ -1003,15 +1225,55 @@
"zha": {
"caption": "ZHA",
"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": {
"header": "Zigbee Home Automation - Add 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": {
"caption": "Z-Wave",
"description": "Manage your Z-Wave network",
"learn_more": "Learn more about Z-Wave",
"common": {
"value": "Value",
"instance": "Instance",
@ -1023,6 +1285,10 @@
"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."
},
"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_stopped": "Z-Wave Network Stopped",
"network_starting": "Starting Z-Wave Network...",
@ -1119,6 +1385,8 @@
"header": "Card Configuration",
"pick_card": "Pick the card you want to add.",
"toggle_editor": "Toggle Editor",
"show_visual_editor": "Show Visual Editor",
"show_code_editor": "Show Code Editor",
"add": "Add Card",
"edit": "Edit",
"delete": "Delete",
@ -1138,17 +1406,29 @@
"migrate": "Migrate config"
},
"card": {
"alarm_panel": {
"alarm-panel": {
"name": "Alarm Panel",
"available_states": "Available States"
},
"conditional": {
"name": "Conditional"
},
"config": {
"required": "Required",
"optional": "Optional"
},
"entities": {
"name": "Entities",
"show_header_toggle": "Show Header Toggle?"
},
"entity-button": {
"name": "Entity Button"
},
"entity-filter": {
"name": "Entity Filter"
},
"gauge": {
"name": "Gauge",
"severity": {
"define": "Define Severity?",
"green": "Green",
@ -1157,8 +1437,21 @@
}
},
"glance": {
"name": "Glance",
"columns": "Columns"
},
"history-graph": {
"name": "History Graph"
},
"horizontal-stack": {
"name": "Horizontal Stack"
},
"iframe": {
"name": "iFrame"
},
"light": {
"name": "Light"
},
"generic": {
"aspect_ratio": "Aspect Ratio",
"camera_image": "Camera Entity",
@ -1184,17 +1477,50 @@
"url": "Url"
},
"map": {
"name": "Map",
"geo_location_sources": "Geolocation Sources",
"dark_mode": "Dark Mode?",
"default_zoom": "Default Zoom",
"source": "Source"
},
"markdown": {
"name": "Markdown",
"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": {
"name": "Sensor",
"graph_detail": "Graph Detail",
"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",
"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": {
"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.",

View File

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

View File

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

View File

@ -160,7 +160,7 @@
"not_home": "Ude"
},
"fan": {
"off": "Slukket",
"off": "Fra",
"on": "Tændt"
},
"group": {
@ -504,7 +504,12 @@
"leave": "Forlad"
},
"device": {
"label": "Enhed"
"label": "Enhed",
"extra_fields": {
"above": "Over",
"below": "Under",
"for": "Varighed"
}
}
},
"learn_more": "Lær om udløsere"
@ -775,7 +780,8 @@
"enabled_label": "Aktivér enhed",
"enabled_cause": "Deaktiveret af {cause}.",
"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": {
@ -829,7 +835,7 @@
"caption": "Gør kun noget, hvis ..."
},
"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."
},
"vibrate": {
"header": "Vibrere",
"header": "Vibrer",
"description": "Aktivér eller deaktiver vibrationer på denne enhed, når du styrer enheder."
}
},
@ -1058,7 +1064,8 @@
"navigate_to": "Naviger til {location}",
"toggle": "Skift {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": {
@ -1113,6 +1120,9 @@
"required": "Påkrævet",
"optional": "Valgfri"
},
"entities": {
"show_header_toggle": "Vis omskifter ved overskrift?"
},
"gauge": {
"severity": {
"define": "Definer alvorlighed?",
@ -1131,6 +1141,7 @@
"entities": "Enheder",
"entity": "Enhed",
"hold_action": "Hold handling",
"hours_to_show": "Vis i antal timer",
"icon": "Ikon",
"icon_height": "Ikonhøjde",
"image": "Sti til billede",
@ -1432,12 +1443,12 @@
"manuf": "af {manufacturer}",
"no_area": "Intet område",
"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",
"remove": "Fjern en enhed fra Zigbee-netværket."
},
"zha_device_card": {
"device_name_placeholder": "Navn",
"device_name_placeholder": "Bruger tildelt navn",
"area_picker_label": "Område",
"update_name_button": "Opdater navn"
}

View File

@ -504,7 +504,12 @@
"leave": "Verlassen"
},
"device": {
"label": "Gerät"
"label": "Gerät",
"extra_fields": {
"above": "Über",
"below": "Unter",
"for": "Dauer"
}
}
},
"learn_more": "Erfahre mehr über Auslöser"
@ -554,6 +559,12 @@
},
"device": {
"label": "Gerät"
},
"and": {
"label": "Und"
},
"or": {
"label": "Oder"
}
},
"learn_more": "Erfahre mehr über Bedingungen"
@ -596,7 +607,11 @@
"learn_more": "Erfahre mehr über Aktionen"
},
"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": {
@ -754,7 +769,8 @@
"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.",
"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": {
"unavailable": "Diese Entität ist derzeit nicht verfügbar.",
@ -763,7 +779,9 @@
"update": "UPDATE",
"enabled_label": "Entität aktivieren",
"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": {
@ -808,7 +826,18 @@
},
"devices": {
"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": {
@ -886,7 +915,8 @@
"description": "Dies blendet die Seitenleiste standardmäßig aus, ähnlich der Nutzung auf Mobilgeräten."
},
"vibrate": {
"header": "Vibrieren"
"header": "Vibrieren",
"description": "Aktivieren oder deaktivieren Sie die Vibration an diesem Gerät, wenn Sie Geräte steuern."
}
},
"page-authorize": {
@ -1034,7 +1064,8 @@
"navigate_to": "Navigiere zu {location}",
"toggle": "{name} umschalten",
"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": {
@ -1080,6 +1111,66 @@
"edit_lovelace": {
"header": "Titel deiner Lovelace UI",
"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": {
@ -1352,6 +1443,7 @@
"manuf": "von {manufacturer}",
"no_area": "Kein Bereich",
"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.",
"remove": "Entfernen Sie ein Gerät aus dem ZigBee-Netzwerk."
},
@ -1437,7 +1529,7 @@
"away": "Abwesend",
"boost": "Maximal",
"comfort": "Komfort",
"home": "Zu Hause",
"home": "Zuhause",
"sleep": "Schlafen",
"activity": "Aktivität"
},

View File

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

View File

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

View File

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

View File

@ -504,7 +504,12 @@
"leave": "Salir"
},
"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"

View File

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

View File

@ -1001,7 +1001,7 @@
},
"core-config": {
"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.",
"location_name_default": "Koti",
"button_detect": "Havaitse",

View File

@ -33,12 +33,12 @@
},
"automation": {
"off": "Off",
"on": "On"
"on": "[%key_id:state::default::on%]"
},
"binary_sensor": {
"default": {
"off": "Off",
"on": "On"
"on": "[%key_id:state::default::on%]"
},
"moisture": {
"off": "Sec",
@ -118,8 +118,8 @@
}
},
"calendar": {
"off": "Off",
"on": "On"
"off": "[%key_id:state::default::off%]",
"on": "[%key_id:state::default::on%]"
},
"camera": {
"recording": "Enregistrement",
@ -165,7 +165,7 @@
},
"group": {
"off": "Off",
"on": "On",
"on": "[%key_id:state::default::on%]",
"home": "Présent",
"not_home": "Absent",
"open": "Ouvert",
@ -210,19 +210,19 @@
"scening": "Scénario"
},
"script": {
"off": "Off",
"on": "On"
"off": "[%key_id:state::default::off%]",
"on": "[%key_id:state::default::on%]"
},
"sensor": {
"off": "Off",
"on": "On"
"off": "[%key_id:state::default::off%]",
"on": "[%key_id:state::default::on%]"
},
"sun": {
"above_horizon": "Au-dessus de l'horizon",
"below_horizon": "Sous lhorizon"
},
"switch": {
"off": "Off",
"off": "[%key_id:state::default::off%]",
"on": "On"
},
"zwave": {
@ -284,7 +284,7 @@
"alarm_control_panel": {
"armed": "Activé",
"disarmed": "Désactivé",
"armed_home": "Armé",
"armed_home": "Activé",
"armed_away": "Activé",
"armed_night": "Activé",
"pending": "En cours",
@ -294,7 +294,7 @@
"armed_custom_bypass": "Activé"
},
"device_tracker": {
"home": "Maison",
"home": "Présent",
"not_home": "Absent"
},
"person": {
@ -504,7 +504,12 @@
"leave": "Quitte"
},
"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"
@ -1524,7 +1529,7 @@
"away": "Absent",
"boost": "Renforcer",
"comfort": "Confort",
"home": "Accueil",
"home": "Présent",
"sleep": "Veille",
"activity": "Activité"
},

View File

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

View File

@ -504,7 +504,12 @@
"leave": "Távozás"
},
"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"

View File

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

View File

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

View File

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

View File

@ -504,7 +504,12 @@
"leave": "퇴장"
},
"device": {
"label": "기기"
"label": "기기",
"extra_fields": {
"above": "이상",
"below": "이하",
"for": "동안"
}
}
},
"learn_more": "트리거에 대해 더 알아보기"
@ -554,6 +559,12 @@
},
"device": {
"label": "기기"
},
"and": {
"label": "다중조건 (그리고)"
},
"or": {
"label": "다중조건 (또는)"
}
},
"learn_more": "조건에 대해 더 알아보기"
@ -596,7 +607,11 @@
"learn_more": "동작에 대해 더 알아보기"
},
"load_error_not_editable": "automations.yaml 의 자동화만 편집할 수 있습니다.",
"load_error_unknown": "자동화를 읽어오는 도중 오류가 발생했습니다 ({err_no})."
"load_error_unknown": "자동화를 읽어오는 도중 오류가 발생했습니다 ({err_no}).",
"description": {
"label": "설명",
"placeholder": "부가설명"
}
}
},
"script": {
@ -711,7 +726,7 @@
"caption": "ZHA",
"description": "Zigbee 홈 자동화 네트워크 관리",
"services": {
"reconfigure": "ZHA 기기를 다시 구성 합니다. (장치 복구). 기기에 문제가 있는 경우 사용해주세요. 기기가 배터리로 작동하는 경우, 이 서비스를 사용할 때 기기가 켜져있고 통신이 가능한 상태인지 확인해주세요.",
"reconfigure": "ZHA 기기를 다시 구성 합니다. (기기 복구). 기기에 문제가 있는 경우 사용해주세요. 기기가 배터리로 작동하는 경우, 이 서비스를 사용할 때 기기가 켜져있고 통신이 가능한 상태인지 확인해주세요.",
"updateDeviceName": "이 기기의 사용자 정의 이름을 기기 레지스트리에 설정합니다.",
"remove": "Zigbee 네트워크에서 기기 제거"
},
@ -754,7 +769,8 @@
"unavailable": "(사용불가)",
"introduction": "Home Assistant 는 구성요소의 식별을 위해 모든 구성요소에 고유한 레지스트리를 부여합니다. 각각의 구성요소들은 자신만의 고유한 구성요소 ID 를 가집니다.",
"introduction2": "구성요소를 편집하여 이름을 대체하거나 구성요소 ID를 변경하고 Home Assistant 에서 항목을 제거할 수 있습니다. 단, 구성요소 편집창에서 구성요소를 삭제해도 구성요소가 완전히 제거되는 것은 아닙니다. 완전히 제거하려면, 아래 링크를 따라 통합 구성요소 페이지에서 제거해주세요.",
"integrations_page": "통합 구성요소 페이지"
"integrations_page": "통합 구성요소 페이지",
"show_disabled": "비활성화 된 구성요소 표시"
},
"editor": {
"unavailable": "이 구성요소는 현재 사용할 수 없습니다.",
@ -763,7 +779,9 @@
"update": "업데이트",
"enabled_label": "구성요소 활성화",
"enabled_cause": "{cause} 에 의해 비활성화 되었습니다.",
"enabled_description": "비활성화 된 구성요소는 Home Assistant 에 추가되지 않습니다."
"enabled_description": "비활성화 된 구성요소는 Home Assistant 에 추가되지 않습니다.",
"confirm_delete": "이 구성요소를 제거 하시겠습니까?",
"confirm_delete2": "구성요소 항목을 제거해도 Home Assistant 에서 실제로 구성요소가 제거되는것은 아닙니다. 완전히 제거하려면, Home Assistant 에서 '{platform}' 통합 구성요소를 제거해주세요."
}
},
"person": {
@ -771,9 +789,9 @@
"description": "Home Assistant 가 추적하는 구성원을 관리합니다",
"detail": {
"name": "이름",
"device_tracker_intro": "이 구성원에게 속한 장치를 선택해주세요.",
"device_tracker_picked": "장치 추적 대상",
"device_tracker_pick": "추적 할 장치 선택"
"device_tracker_intro": "이 구성원에게 속한 기기를 선택해주세요.",
"device_tracker_picked": "추적 대상 기기",
"device_tracker_pick": "추적 할 기기 선택"
}
},
"server_control": {
@ -808,7 +826,18 @@
},
"devices": {
"caption": "기기",
"description": "연결된 기기 관리"
"description": "연결된 기기 관리",
"automation": {
"triggers": {
"caption": "...일 때 뭔가를 실행"
},
"conditions": {
"caption": "...인 경우 뭔가를 실행"
},
"actions": {
"caption": "뭔가 트리거 되었을 때...."
}
}
}
},
"profile": {
@ -884,6 +913,10 @@
"force_narrow": {
"header": "항상 사이드바 숨기기",
"description": "모바일 환경과 마찬가지로 기본적으로 사이드 바가 숨겨집니다."
},
"vibrate": {
"header": "진동효과",
"description": "기기를 제어 할 때 이 기기에서 진동을 활성화 또는 비활성화합니다."
}
},
"page-authorize": {
@ -1031,7 +1064,8 @@
"navigate_to": "{location} 로(으로) 이동",
"toggle": "{name} 토글",
"call_service": "{name} 서비스 호출",
"more_info": "추가 정보 표시: {name}"
"more_info": "추가 정보 표시: {name}",
"url": "{url_path} 창 열기"
}
},
"editor": {
@ -1077,6 +1111,66 @@
"edit_lovelace": {
"header": "Lovelace UI 의 제목",
"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": {
@ -1349,7 +1443,7 @@
"manuf": "{manufacturer} 제조",
"no_area": "영역 없음",
"services": {
"reconfigure": "ZHA 장치를 다시 구성 합니다. (장치 복구). 장치에 문제가 있는 경우 사용해주세요. 장치가 배터리로 작동하는 경우, 이 서비스를 사용할 때 장치가 켜져있고 통신이 가능한 상태여야 합니다.",
"reconfigure": "ZHA 기기를 다시 구성 합니다. (기기 복구). 기기에 문제가 있는 경우 사용해주세요. 기기가 배터리로 작동하는 경우, 이 서비스를 사용할 때 기기가 켜져있고 통신이 가능한 상태인지 확인해주세요.",
"updateDeviceName": "이 기기의 사용자 정의 이름을 기기 레지스트리에 설정합니다.",
"remove": "Zigbee 네트워크에서 기기를 제거해주세요."
},
@ -1367,7 +1461,7 @@
},
"notification_drawer": {
"click_to_configure": "버튼을 클릭하여 {entity} 을(를) 구성",
"empty": "알림 없음",
"empty": "알림 내용이 없습니다.",
"title": "알림"
}
},
@ -1381,7 +1475,7 @@
"configurator": "구성",
"conversation": "대화",
"cover": "여닫이",
"device_tracker": "추적 장치",
"device_tracker": "추적 기기",
"fan": "송풍기",
"history_graph": "기록그래프",
"group": "그룹",

View File

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

View File

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

View File

@ -753,7 +753,7 @@
"header": "Vienību Reģistrs",
"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.",
"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"
},
"editor": {
@ -1098,7 +1098,7 @@
"page-demo": {
"cards": {
"demo": {
"demo_by": "{name} ",
"demo_by": "{name}",
"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.",
"learn_more": "Uzziniet vairāk par Home Assistant"
@ -1316,7 +1316,7 @@
"dialogs": {
"more_info_settings": {
"save": "Saglabāt",
"name": "Nosaukuma ignorēšana",
"name": "Piesķirt nosaukumu",
"entity_id": "Vienības ID"
},
"more_info_control": {

View File

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

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